diff --git a/hosts/lab/dokuwiki.nix b/hosts/lab/dokuwiki.nix index db94e8e..3c7ee21 100644 --- a/hosts/lab/dokuwiki.nix +++ b/hosts/lab/dokuwiki.nix @@ -4,7 +4,7 @@ extraConfig = '' @health path /health-ping handle @health { - reverse_proxy localhost:8070 + respond 200 } handle { diff --git a/hosts/lab/sites.nix b/hosts/lab/sites.nix index 3955272..89f1605 100644 --- a/hosts/lab/sites.nix +++ b/hosts/lab/sites.nix @@ -15,11 +15,16 @@ # dataDir — base directory for repo and data (default: /srv/) # readWritePaths — paths the server can write to at runtime (default: []) # afterServices — systemd units to wait for before building (default: ["forgejo.service"]) +# preview.enable — TinyAuth-protected preview of this site (default: false) +# preview.branch — branch for preview (default: "develop") +# preview.domain — preview domain (default: 0.ily.rs) +# preview.port — preview server port (required when static = false) # # remarks: # # - a listener is active on http://localhost:4323/hooks/${name}-rebuild for CD # Forgejo repo -> settings -> Webhooks -> Add webhook +# - preview webhook: http://localhost:4323/hooks/${name}-preview-rebuild { ... }: let @@ -48,6 +53,7 @@ in redirectDomains = [ "penfield.wynne.rs" ]; repo = "https://git.ily.rs/lew/penfield"; static = true; + preview.enable = true; }; services.site.record-generator = { diff --git a/modules/site.nix b/modules/site.nix index 1e73bcd..da00a3d 100644 --- a/modules/site.nix +++ b/modules/site.nix @@ -85,10 +85,32 @@ let default = [ "forgejo.service" ]; description = "Systemd units to wait for before building."; }; + + preview = { + enable = lib.mkEnableOption "TinyAuth-protected preview of this site"; + + branch = mkOption { + type = types.str; + default = "develop"; + }; + + domain = mkOption { + type = types.str; + default = "0${name}.ily.rs"; + description = "Preview domain. Defaults to 0.ily.rs."; + }; + + port = mkOption { + type = types.nullOr types.port; + default = null; + description = "Port for preview Node.js server. Required when parent static = false."; + }; + }; }; }); cfg = lib.filterAttrs (_: site: site.enable) config.services.site; + previewCfg = lib.filterAttrs (_: site: site.enable && site.preview.enable) config.services.site; webhookPort = 4323; @@ -113,7 +135,12 @@ in }; config = { - services.caddy.virtualHosts = mkMerge (mapAttrsToList (name: site: + assertions = mapAttrsToList (name: site: { + assertion = site.static || site.preview.port != null; + message = "services.site.${name}.preview.port is required when static = false and preview is enabled"; + }) previewCfg; + + services.caddy.virtualHosts = mkMerge ((mapAttrsToList (name: site: { ${site.domain}.extraConfig = if site.static then '' root * ${site.dataDir}/repo/${site.buildOutputDir} @@ -130,7 +157,33 @@ in redir https://${site.domain}{uri} permanent ''; }) site.redirectDomains) - ) cfg); + ) cfg) ++ (mapAttrsToList (name: site: + let previewDataDir = "/srv/${name}-preview"; in { + ${site.preview.domain}.extraConfig = if site.static then '' + @health path /health-ping + handle @health { + respond 200 + } + handle { + import tinyauth + root * ${previewDataDir}/repo/${site.buildOutputDir} + encode zstd gzip + try_files {path} /index.html + file_server + } + '' else '' + @health path /health-ping + handle @health { + respond 200 + } + handle { + import tinyauth + reverse_proxy localhost:${toString site.preview.port} + encode zstd gzip + } + ''; + } + ) previewCfg)); systemd.services = mkMerge ((mapAttrsToList (name: site: let h = siteHelpers name site; in { @@ -184,7 +237,63 @@ in }; }; } - ) cfg) ++ [{ + ) cfg) ++ (mapAttrsToList (name: site: + let + h = siteHelpers name site; + previewDataDir = "/srv/${name}-preview"; + previewUser = "${name}-preview"; + in { + "${name}-preview-rebuild" = { + description = "Clone/pull and build preview of ${site.domain}"; + after = [ "network-online.target" ] ++ site.afterServices; + path = [ pkgs.nodejs pkgs.bash ] + ++ optional (site.packageManager == "pnpm") pkgs.pnpm; + environment = site.buildEnvironment; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = false; + ExecStartPre = "+${pkgs.writeShellScript "prepare-${name}-preview" '' + mkdir -p ${previewDataDir} + chown -R ${previewUser}:${previewUser} ${previewDataDir} + ''}"; + ExecStart = pkgs.writeShellScript "rebuild-${name}-preview" '' + set -euo pipefail + if [ ! -d ${previewDataDir}/repo/.git ]; then + ${pkgs.git}/bin/git clone ${site.repo} ${previewDataDir}/repo + fi + cd ${previewDataDir}/repo + ${pkgs.git}/bin/git fetch origin + ${pkgs.git}/bin/git reset --hard origin/${site.preview.branch} + ${h.installCmd} + ${h.pmBin} run build + ''; + ExecStartPost = lib.mkIf (!site.static) + "+/run/current-system/sw/bin/systemctl restart ${previewUser}"; + User = previewUser; + Group = previewUser; + }; + }; + } // lib.optionalAttrs (!site.static) { + ${previewUser} = { + description = "Preview of ${site.domain}"; + environment = { + HOST = "127.0.0.1"; + PORT = toString site.preview.port; + } // site.environment; + serviceConfig = { + Type = "simple"; + WorkingDirectory = "${previewDataDir}/repo"; + ExecStart = "${pkgs.nodejs}/bin/node ${site.entryPoint}"; + Restart = "on-failure"; + User = previewUser; + Group = previewUser; + ReadWritePaths = site.readWritePaths; + }; + }; + } + ) previewCfg) ++ [{ site-webhook = mkIf (cfg != {}) { description = "Webhook listener for site rebuilds"; after = [ "network.target" ]; @@ -192,13 +301,19 @@ in serviceConfig = { Type = "simple"; ExecStart = let - allHooks = mapAttrsToList (name: site: { + allHooks = (mapAttrsToList (name: site: { id = "${name}-rebuild"; execute-command = "/run/current-system/sw/bin/touch"; pass-arguments-to-command = [ { source = "string"; name = "/run/site-rebuild/${name}"; } ]; - }) cfg; + }) cfg) ++ (mapAttrsToList (name: site: { + id = "${name}-preview-rebuild"; + execute-command = "/run/current-system/sw/bin/touch"; + pass-arguments-to-command = [ + { source = "string"; name = "/run/site-rebuild/${name}-preview"; } + ]; + }) previewCfg); hooksFile = pkgs.writeText "site-hooks.json" (builtins.toJSON allHooks); in "${pkgs.webhook}/bin/webhook -hooks ${hooksFile} -port ${toString webhookPort} -verbose"; Restart = "always"; @@ -208,7 +323,7 @@ in }; }]); - systemd.paths = mkMerge (mapAttrsToList (name: site: { + systemd.paths = mkMerge ((mapAttrsToList (name: site: { "${name}-rebuild-trigger" = { description = "Watch for ${name} rebuild trigger"; wantedBy = [ "multi-user.target" ]; @@ -217,18 +332,35 @@ in Unit = "${name}-rebuild.service"; }; }; - }) cfg); + }) cfg) ++ (mapAttrsToList (name: site: { + "${name}-preview-rebuild-trigger" = { + description = "Watch for ${name}-preview rebuild trigger"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathModified = "/run/site-rebuild/${name}-preview"; + Unit = "${name}-preview-rebuild.service"; + }; + }; + }) previewCfg)); - users.users = mkMerge (mapAttrsToList (name: site: { + users.users = mkMerge ((mapAttrsToList (name: site: { ${name} = { isSystemUser = true; group = name; home = site.dataDir; }; - }) cfg); + }) cfg) ++ (mapAttrsToList (name: site: { + "${name}-preview" = { + isSystemUser = true; + group = "${name}-preview"; + home = "/srv/${name}-preview"; + }; + }) previewCfg)); - users.groups = mkMerge (mapAttrsToList (name: _: { + users.groups = mkMerge ((mapAttrsToList (name: _: { ${name} = {}; - }) cfg); + }) cfg) ++ (mapAttrsToList (name: _: { + "${name}-preview" = {}; + }) previewCfg)); }; }