diff --git a/nixos/configuration.nix b/nixos/configuration.nix index 8439662c..c2af38af 100644 --- a/nixos/configuration.nix +++ b/nixos/configuration.nix @@ -42,6 +42,7 @@ ./wsl.nix ./wyoming.nix ./xmonad.nix + ./org-agenda-api-host.nix ]; options = { diff --git a/nixos/flake.nix b/nixos/flake.nix index 56b78a18..a756c675 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -223,6 +223,20 @@ }; defaultConfigurationParams = builtins.listToAttrs (map mkConfigurationParams machineFilenames); + # Build org-agenda-api container for a given system + mkOrgAgendaApiContainer = system: let + pkgs = import nixpkgs { inherit system; }; + orgApiRev = builtins.substring 0 7 (org-agenda-api.rev or "unknown"); + dotfilesRev = builtins.substring 0 7 (self.rev or self.dirtyRev or "dirty"); + dotfilesOrgApi = import ./org-agenda-api.nix { + inherit pkgs system inputs; + }; + tangledConfig = dotfilesOrgApi.org-agenda-custom-config; + containerLib = import ../org-agenda-api/container.nix { + inherit pkgs system tangledConfig org-agenda-api orgApiRev dotfilesRev; + }; + in containerLib.containers.colonelpanic; + customParams = { biskcomp = { system = "aarch64-linux"; @@ -230,6 +244,11 @@ air-gapped-pi = { system = "aarch64-linux"; }; + railbird-sf = { + specialArgs = { + orgAgendaApiContainer = mkOrgAgendaApiContainer "x86_64-linux"; + }; + }; }; mkConfig = { system ? "x86_64-linux", diff --git a/nixos/machines/railbird-sf.nix b/nixos/machines/railbird-sf.nix index 7355a528..5a410324 100644 --- a/nixos/machines/railbird-sf.nix +++ b/nixos/machines/railbird-sf.nix @@ -1,11 +1,26 @@ -{ config, lib, pkgs, forEachUser, ... }: +{ config, lib, pkgs, forEachUser, inputs, orgAgendaApiContainer ? null, ... }: { imports = [ ../configuration.nix + inputs.agenix.nixosModules.default ]; networking.hostName = "railbird-sf"; + # org-agenda-api hosting with nginx + Let's Encrypt + age.secrets.org-api-env = { + file = ../secrets/org-api-passwords.age; + # Readable by the podman container service + }; + + services.org-agenda-api-host = { + enable = true; + domain = "rbsf.tplinkdns.com"; + containerImage = "colonelpanic-org-agenda-api"; + containerImageFile = orgAgendaApiContainer; + secretsFile = config.age.secrets.org-api-env.path; + }; + hardware.enableRedistributableFirmware = true; boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ]; boot.initrd.kernelModules = [ ]; diff --git a/nixos/org-agenda-api-host.nix b/nixos/org-agenda-api-host.nix new file mode 100644 index 00000000..dba01b35 --- /dev/null +++ b/nixos/org-agenda-api-host.nix @@ -0,0 +1,137 @@ +# org-agenda-api-host.nix - Host org-agenda-api container with nginx + Let's Encrypt +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.org-agenda-api-host; + # Random high port to avoid conflicts + containerPort = 51847; +in +{ + options.services.org-agenda-api-host = { + enable = mkEnableOption "org-agenda-api container hosting"; + + domain = mkOption { + type = types.str; + default = "rbsf.tplinkdns.com"; + description = "Domain name for the service (used for Let's Encrypt)"; + }; + + acmeEmail = mkOption { + type = types.str; + default = "IvanMalison@gmail.com"; + description = "Email for Let's Encrypt certificate notifications"; + }; + + containerImageFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Nix-built container image (tarball from dockerTools)"; + }; + + containerImage = mkOption { + type = types.str; + default = "colonelpanic-org-agenda-api"; + description = "Container image name (used when imageFile is provided)"; + }; + + gitSyncRepository = mkOption { + type = types.str; + default = "git@github.com:colonelpanic8/org.git"; + description = "Git repository to sync org files from"; + }; + + gitUserEmail = mkOption { + type = types.str; + default = "IvanMalison@gmail.com"; + description = "Git user email for commits"; + }; + + gitUserName = mkOption { + type = types.str; + default = "Ivan Malison"; + description = "Git user name for commits"; + }; + + authUser = mkOption { + type = types.str; + default = "imalison"; + description = "Basic auth username"; + }; + + secretsFile = mkOption { + type = types.path; + description = "Path to agenix-decrypted secrets file containing AUTH_PASSWORD and GIT_SSH_PRIVATE_KEY"; + }; + + timezone = mkOption { + type = types.str; + default = "America/Los_Angeles"; + description = "Timezone for the container"; + }; + }; + + config = mkIf cfg.enable { + # Enable ACME for Let's Encrypt + security.acme = { + acceptTerms = true; + defaults.email = cfg.acmeEmail; + }; + + # Nginx reverse proxy with TLS + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + + virtualHosts.${cfg.domain} = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:${toString containerPort}"; + proxyWebsockets = true; + extraConfig = '' + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + ''; + }; + }; + }; + + # Open firewall for HTTP/HTTPS + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + # Container service using podman + virtualisation.oci-containers = { + backend = "podman"; + containers.org-agenda-api = { + image = cfg.containerImage; + imageFile = cfg.containerImageFile; + autoStart = true; + ports = [ "127.0.0.1:${toString containerPort}:80" ]; + environment = { + TZ = cfg.timezone; + GIT_SYNC_REPOSITORY = cfg.gitSyncRepository; + GIT_USER_EMAIL = cfg.gitUserEmail; + GIT_USER_NAME = cfg.gitUserName; + AUTH_USER = cfg.authUser; + }; + environmentFiles = [ cfg.secretsFile ]; + extraOptions = [ + "--pull=never" # Image is from nix store, don't try to pull + ]; + }; + }; + + # Ensure container restarts on failure + systemd.services.podman-org-agenda-api = { + serviceConfig = { + Restart = "always"; + RestartSec = "10s"; + }; + }; + }; +} diff --git a/nixos/secrets/secrets.nix b/nixos/secrets/secrets.nix index d465139b..8c4c3d25 100644 --- a/nixos/secrets/secrets.nix +++ b/nixos/secrets/secrets.nix @@ -20,7 +20,7 @@ in "discourse-admin-password.age".publicKeys = keys.hostKeys; "discourse-secret-key-base.age".publicKeys = keys.hostKeys; "vaultwarden-environment-file.age".publicKeys = keys.hostKeys; - "org-api-passwords.age".publicKeys = keys.hostKeys ++ keys.kanivanKeys; + "org-api-passwords.age".publicKeys = keys.hostKeys ++ keys.kanivanKeys ++ keys.railbird-sf; "google-assistant-integration-service-key.age".publicKeys = keys.hostKeys ++ keys.kanivanKeys; "zwave-js.json.age".publicKeys = keys.hostKeys ++ keys.kanivanKeys; }