From 98833bdc6fad9d0ec4534ae5551661db1461c97c Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Sun, 29 Dec 2024 01:28:33 -0500
Subject: [PATCH] feat(caddy): move to new  package

---
 apps/update-sources/default.nix               |   2 +
 apps/update-sources/src/app.ts                |  13 +++
 apps/update-sources/src/misc.ts               |  77 ++++++++++++-
 .../modules/{caddy.nix => caddy/default.nix}  |  25 +++--
 .../cluster/modules/caddy/plugins.nix         |  11 ++
 configurations/cluster/modules/default.nix    |   2 +-
 flake.lock                                    |  21 ----
 flake.nix                                     |   6 -
 inputs/default.nix                            |   5 -
 modules/caddy-plus/default.nix                | 104 ++++++++++++++++++
 modules/caddy-plus/sub-dir-options.nix        |  43 ++++++++
 modules/caddy-plus/sub-domain-options.nix     |  52 +++++++++
 modules/caddy-plus/vhost-options.nix          |  51 +++++++++
 modules/default.nix                           |   1 +
 14 files changed, 367 insertions(+), 46 deletions(-)
 rename configurations/cluster/modules/{caddy.nix => caddy/default.nix} (93%)
 create mode 100644 configurations/cluster/modules/caddy/plugins.nix
 create mode 100644 modules/caddy-plus/default.nix
 create mode 100644 modules/caddy-plus/sub-dir-options.nix
 create mode 100644 modules/caddy-plus/sub-domain-options.nix
 create mode 100644 modules/caddy-plus/vhost-options.nix

diff --git a/apps/update-sources/default.nix b/apps/update-sources/default.nix
index 1e128932..f00382d2 100644
--- a/apps/update-sources/default.nix
+++ b/apps/update-sources/default.nix
@@ -1,6 +1,7 @@
 {
   buildApp,
   callPackage,
+  go,
   nix-update,
   nodejs_latest,
   prefetch-npm-deps,
@@ -11,6 +12,7 @@ buildApp {
   npmDepsHash = "sha256-TU7HoUGeCXUKwm2s4Np6NQahk6gWBN9WC5vob3zw7Ns=";
 
   runtimeInputs = [
+    go
     nix-update
     nodejs_latest
     prefetch-npm-deps
diff --git a/apps/update-sources/src/app.ts b/apps/update-sources/src/app.ts
index 1717903b..2d30620d 100644
--- a/apps/update-sources/src/app.ts
+++ b/apps/update-sources/src/app.ts
@@ -10,6 +10,7 @@ import updateNodeModules from './node-modules';
 
 import {
     runNixUpdate,
+    updateCaddyPlugins,
     updateCustomPackage,
     updateVuetorrent,
 } from './misc';
@@ -66,6 +67,10 @@ const main = async() => {
         console.log(runNixUpdate('homepage'));
     }
 
+    if (args['cp'] || args['caddy-plugins']) {
+        console.log(updateCaddyPlugins());
+    }
+
     if (args['a'] || args['all']) {
         // Update this first because of nix run cmd
         const firefoxOutput = updateFirefoxAddons();
@@ -93,6 +98,11 @@ const main = async() => {
         console.log(vuetorrentOutput);
 
 
+        const caddyPluginsOutput = updateCaddyPlugins();
+
+        console.log(caddyPluginsOutput);
+
+
         // This doesn't need to be added to commit msgs
         console.log(updateCustomPackage(
             'scopedPackages.x86_64-linux.lovelace-components.custom-sidebar',
@@ -144,6 +154,9 @@ const main = async() => {
         if (vuetorrentOutput.length > 5) {
             output.push(`Misc Sources:\n${indentOutput(vuetorrentOutput)}\n\n`);
         }
+        if (caddyPluginsOutput.length > 5) {
+            output.push(`Caddy Plugins:\n${indentOutput(caddyPluginsOutput)}\n\n`);
+        }
         if (nixUpdateOutputs.length > 5) {
             output.push(`nix-update executions:\n${indentOutput(nixUpdateOutputs)}\n`);
         }
diff --git a/apps/update-sources/src/misc.ts b/apps/update-sources/src/misc.ts
index 2ea33ce1..893e3f29 100644
--- a/apps/update-sources/src/misc.ts
+++ b/apps/update-sources/src/misc.ts
@@ -1,7 +1,7 @@
 import { writeFileSync } from 'node:fs';
 import { spawnSync } from 'node:child_process';
 
-import { parseFetchurl } from './lib';
+import { parseFetchurl, replaceInFile } from './lib';
 
 
 /* Constants */
@@ -71,3 +71,78 @@ export const runNixUpdate = (
         stderr: execution.stderr.toString(),
     };
 };
+
+
+const genPluginsText = (
+    plugins: Record<string, { url: string, version: string }>,
+) => `# This file was autogenerated. DO NOT EDIT!
+{
+  plugins = {
+${Object.entries(plugins)
+    .map(([key, value]) => `
+    ${key} = {
+      url = "${value.url}";
+      version = "${value.version}";
+    };
+    `)
+    .join('')}
+  };
+
+  hash = "";
+}
+`;
+
+export const updateCaddyPlugins = () => {
+    let updates = '';
+    const dir = `${FLAKE}/configurations/cluster/modules/caddy`;
+
+    // Setup workspace
+    spawnSync(
+        [
+            'rm -rf /tmp/update-caddy',
+            'mkdir -p /tmp/update-caddy',
+            'cd /tmp/update-caddy || exit 1',
+            'go mod init temp',
+        ].join('; '),
+        [],
+        { shell: true, cwd: '/tmp' },
+    );
+
+    const plugins = JSON.parse(spawnSync('nix',
+        ['eval', '-f', `${dir}/plugins.nix`, '--json'],
+        { shell: true }).stdout.toString()).plugins as Record<string, { url: string, version: string }>;
+
+    // Get most recent versions of plugins
+    Object.entries(plugins).forEach(([key, value]) => {
+        const NEW_VERSION = spawnSync([
+            'go mod init temp > /dev/null',
+            `go get ${value.url} > /dev/null`,
+            `grep '${value.url}' go.mod`,
+        ].join('; '), [], { shell: true, cwd: '/tmp/update-caddy' })
+            .stdout
+            .toString()
+            .trim()
+            .replace(' // indirect', '')
+            .split(' ')[1];
+
+        if (plugins[key].version !== NEW_VERSION) {
+            updates += `${key}: ${plugins[key].version} -> ${NEW_VERSION}`;
+            plugins[key].version = NEW_VERSION;
+        }
+    });
+
+    writeFileSync(`${dir}/plugins.nix`, genPluginsText(plugins));
+
+    // Get new hash
+    const caddyPkgAttr = 'nixosConfigurations.thingone.config.services.caddy.package';
+
+    const NEW_HASH = spawnSync(
+        `nix build "$FLAKE#${caddyPkgAttr}" |& sed -n 's/.*got: *//p'`,
+        [],
+        { shell: true },
+    ).stdout.toString().trim();
+
+    replaceInFile(/hash = ".*";/, `hash = "${NEW_HASH}";`, `${dir}/plugins.nix`);
+
+    return updates;
+};
diff --git a/configurations/cluster/modules/caddy.nix b/configurations/cluster/modules/caddy/default.nix
similarity index 93%
rename from configurations/cluster/modules/caddy.nix
rename to configurations/cluster/modules/caddy/default.nix
index 507e0f41..c7b7601e 100644
--- a/configurations/cluster/modules/caddy.nix
+++ b/configurations/cluster/modules/caddy/default.nix
@@ -1,36 +1,37 @@
 {
-  caddy-plugins,
   config,
+  lib,
   mainUser,
   pkgs,
+  self,
   ...
 }: let
+  inherit (lib) attrValues;
+
   inherit (config.sops) secrets;
   inherit (config.networking) hostName;
-
-  caddy = caddy-plugins.packages.${pkgs.system}.default;
 in {
-  imports = [caddy-plugins.nixosModules.default];
+  imports = [self.nixosModules.caddy-plus];
 
-  # User stuff
-  environment.systemPackages = [caddy];
   users.users.${mainUser}.extraGroups = ["caddy"];
 
   boot.kernel.sysctl."net.ipv4.ip_nonlocal_bind" = 1;
 
   systemd.services.caddy.serviceConfig = {
     EnvironmentFile = secrets.caddy-cloudflare.path;
-
-    # For some reason the service
-    # doesn't shutdown normally
-    KillSignal = "SIGKILL";
-    RestartKillSignal = "SIGKILL";
   };
 
   services.caddy = {
     enable = true;
     enableReload = false;
-    package = caddy;
+
+    package = let
+      pluginsInfo = import ./plugins.nix;
+    in
+      pkgs.caddy.withPlugins {
+        plugins = map (x: "${x.url}@${x.version}") (attrValues pluginsInfo.plugins);
+        inherit (pluginsInfo) hash;
+      };
 
     virtualHosts = let
       clusterIP = config.services.pcsd.virtualIps.caddy-vip.ip;
diff --git a/configurations/cluster/modules/caddy/plugins.nix b/configurations/cluster/modules/caddy/plugins.nix
new file mode 100644
index 00000000..f5a65488
--- /dev/null
+++ b/configurations/cluster/modules/caddy/plugins.nix
@@ -0,0 +1,11 @@
+# This file was autogenerated. DO NOT EDIT!
+{
+  plugins = {
+    cloudflare = {
+      url = "github.com/caddy-dns/cloudflare";
+      version = "v0.0.0-20240703190432-89f16b99c18e";
+    };
+  };
+
+  hash = "sha256-WGV/Ve7hbVry5ugSmTYWDihoC9i+D3Ct15UKgdpYc9U=";
+}
diff --git a/configurations/cluster/modules/default.nix b/configurations/cluster/modules/default.nix
index 2d8e70fb..9233a79c 100644
--- a/configurations/cluster/modules/default.nix
+++ b/configurations/cluster/modules/default.nix
@@ -1,7 +1,7 @@
 {...}: {
   imports = [
     ./blocky.nix
-    ./caddy.nix
+    ./caddy
     ./headscale
     ./nfs-client.nix
     ./pcsd.nix
diff --git a/flake.lock b/flake.lock
index a8197732..1792aed1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -128,26 +128,6 @@
         "type": "github"
       }
     },
-    "caddy-plugins": {
-      "inputs": {
-        "nixpkgs": [
-          "nixpkgs"
-        ]
-      },
-      "locked": {
-        "lastModified": 1734463590,
-        "narHash": "sha256-SoS0sM/UMf3uGRPor7xuYlJdt49nLDqxVNEAfWk2G/I=",
-        "owner": "matt1432",
-        "repo": "nixos-caddy-cloudflare",
-        "rev": "b6cecba0d5b3c6eded63751f202fb4877db3f776",
-        "type": "github"
-      },
-      "original": {
-        "owner": "matt1432",
-        "repo": "nixos-caddy-cloudflare",
-        "type": "github"
-      }
-    },
     "caule-themes-src": {
       "flake": false,
       "locked": {
@@ -1691,7 +1671,6 @@
         "astal": "astal",
         "bat-theme-src": "bat-theme-src",
         "bazarr-bulk": "bazarr-bulk",
-        "caddy-plugins": "caddy-plugins",
         "caule-themes-src": "caule-themes-src",
         "custom-sidebar-src": "custom-sidebar-src",
         "discord-overlay": "discord-overlay",
diff --git a/flake.nix b/flake.nix
index db600794..e01d18e8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -28,12 +28,6 @@
       repo = "bazarr-bulk";
       type = "github";
     };
-    caddy-plugins = {
-      inputs.nixpkgs.follows = "nixpkgs";
-      owner = "matt1432";
-      repo = "nixos-caddy-cloudflare";
-      type = "github";
-    };
     caule-themes-src = {
       flake = false;
       owner = "ricardoquecria";
diff --git a/inputs/default.nix b/inputs/default.nix
index 6d3438c6..c62b03c3 100644
--- a/inputs/default.nix
+++ b/inputs/default.nix
@@ -73,11 +73,6 @@ let
       owner = "matt1432";
       repo = "nixos-pcsd";
     };
-
-    caddy-plugins = mkDep {
-      owner = "matt1432";
-      repo = "nixos-caddy-cloudflare";
-    };
   };
 
   serviviInputs = {
diff --git a/modules/caddy-plus/default.nix b/modules/caddy-plus/default.nix
new file mode 100644
index 00000000..d6bd0aea
--- /dev/null
+++ b/modules/caddy-plus/default.nix
@@ -0,0 +1,104 @@
+self: {
+  config,
+  lib,
+  pkgs,
+  ...
+}: let
+  inherit (lib) types;
+  inherit (lib.attrsets) attrValues;
+  inherit (lib.modules) mkIf mkOverride;
+  inherit (lib.options) mkOption;
+  inherit (lib.strings) concatMapStringsSep concatStringsSep optionalString stringLength substring toUpper;
+
+  cfg = config.services.caddy;
+
+  capitalize = str:
+    toUpper (substring 0 1 str)
+    + substring 1 (stringLength str) str;
+
+  mkSubDirConf = subOpts:
+    optionalString (subOpts.reverseProxy != null) (
+      if subOpts.experimental
+      then ''
+        ${subOpts.extraConfig}
+
+        redir /${subOpts.subDirName} /${subOpts.subDirName}/
+        route /${subOpts.subDirName}/* {
+          uri strip_prefix ${subOpts.subDirName}
+          reverse_proxy ${subOpts.reverseProxy} {
+            header_up X-Real-IP {remote}
+            header_up X-${capitalize (subOpts.subDirName)}-Base "/${subOpts.subDirName}"
+          }
+        }
+      ''
+      else ''
+        ${subOpts.extraConfig}
+
+        redir /${subOpts.subDirName} /${subOpts.subDirName}/
+        reverse_proxy /${subOpts.subDirName}/* {
+          to ${subOpts.reverseProxy}
+        }
+      ''
+    );
+
+  mkSubDomainConf = hostName: subOpts: ''
+    @${subOpts.subDomainName} host ${subOpts.subDomainName}.${hostName}
+    handle @${subOpts.subDomainName} {
+      ${subOpts.extraConfig}
+      ${optionalString (subOpts.reverseProxy != null) "reverse_proxy ${subOpts.reverseProxy}"}
+
+      ${concatMapStringsSep "\n" mkSubDirConf (attrValues subOpts.subDirectories)}
+    }
+  '';
+
+  mkVHostConf = hostOpts: let
+    sslCertDir = config.security.acme.certs.${hostOpts.useACMEHost}.directory;
+  in ''
+    ${hostOpts.hostName} ${concatStringsSep " " hostOpts.serverAliases} {
+      ${optionalString (hostOpts.listenAddresses != []) "bind ${concatStringsSep " " hostOpts.listenAddresses}"}
+      ${optionalString (hostOpts.useACMEHost != null) "tls ${sslCertDir}/cert.pem ${sslCertDir}/key.pem"}
+      log {
+        ${hostOpts.logFormat}
+      }
+
+      ${hostOpts.extraConfig}
+      ${optionalString (hostOpts.reverseProxy != null) "reverse_proxy ${hostOpts.reverseProxy}"}
+      ${concatMapStringsSep "\n" mkSubDirConf (attrValues hostOpts.subDirectories)}
+      ${concatMapStringsSep "\n" (mkSubDomainConf hostOpts.hostName) (attrValues hostOpts.subDomains)}
+    }
+  '';
+
+  settingsFormat = pkgs.formats.json {};
+
+  configFile =
+    if cfg.settings != {}
+    then settingsFormat.generate "caddy.json" cfg.settings
+    else let
+      Caddyfile = pkgs.writeTextDir "Caddyfile" ''
+        {
+          ${cfg.globalConfig}
+        }
+        ${cfg.extraConfig}
+        ${concatMapStringsSep "\n" mkVHostConf (attrValues cfg.virtualHosts)}
+      '';
+
+      Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" {nativeBuildInputs = [cfg.package];} ''
+        mkdir -p $out
+        cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
+        caddy fmt --overwrite $out/Caddyfile
+      '';
+    in "${
+      if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
+      then Caddyfile-formatted
+      else Caddyfile
+    }/Caddyfile";
+in {
+  options.services.caddy.virtualHosts = mkOption {
+    type = types.attrsOf (types.submodule (import ./vhost-options.nix {inherit cfg;}));
+  };
+
+  # implementation
+  config = mkIf cfg.enable {
+    services.caddy.configFile = mkOverride 80 configFile;
+  };
+}
diff --git a/modules/caddy-plus/sub-dir-options.nix b/modules/caddy-plus/sub-dir-options.nix
new file mode 100644
index 00000000..fb97d337
--- /dev/null
+++ b/modules/caddy-plus/sub-dir-options.nix
@@ -0,0 +1,43 @@
+{cfg}: {
+  lib,
+  name,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in {
+  options = {
+    subDirName = mkOption {
+      type = types.str;
+      default = name;
+      description = "The sub directory name to handle.";
+    };
+
+    reverseProxy = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Option to give the parameters to a simple "reverse_proxy" command
+        appended after extraConfig.
+      '';
+    };
+
+    experimental = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Specify if the app being proxied expects to be under a subdirectory.
+        If it doesn't, we can attempt to circumvent that but it is not guaranteed
+        to work for every app.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Additional lines of configuration appended to this sub domain in the
+        automatically generated `Caddyfile`.
+      '';
+    };
+  };
+}
diff --git a/modules/caddy-plus/sub-domain-options.nix b/modules/caddy-plus/sub-domain-options.nix
new file mode 100644
index 00000000..321bd249
--- /dev/null
+++ b/modules/caddy-plus/sub-domain-options.nix
@@ -0,0 +1,52 @@
+{cfg}: {
+  lib,
+  name,
+  ...
+}: let
+  inherit (lib) literalExpression mkOption types;
+in {
+  options = {
+    subDomainName = mkOption {
+      type = types.str;
+      default = name;
+      description = "The sub domain name to handle.";
+    };
+
+    reverseProxy = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Option to give the parameters to a simple "reverse_proxy" command
+        appended after extraConfig.
+      '';
+    };
+
+    subDirectories = mkOption {
+      type = types.attrsOf (types.submodule (import ./sub-dir-options.nix {inherit cfg;}));
+      default = {};
+      example = literalExpression ''
+        {
+          headscale = {
+            appSupport = false;
+            reverseProxy = "localhost:8080";
+            extraConfig = '''
+              encode gzip
+            ''';
+          };
+        };
+      '';
+      description = ''
+        Declarative specification of a subdomain's subdirectories served by Caddy.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        Additional lines of configuration appended to this sub domain in the
+        automatically generated `Caddyfile`.
+      '';
+    };
+  };
+}
diff --git a/modules/caddy-plus/vhost-options.nix b/modules/caddy-plus/vhost-options.nix
new file mode 100644
index 00000000..8a3e0baf
--- /dev/null
+++ b/modules/caddy-plus/vhost-options.nix
@@ -0,0 +1,51 @@
+{cfg}: {lib, ...}: let
+  inherit (lib) literalExpression mkOption types;
+in {
+  options = {
+    reverseProxy = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = ''
+        Option to give the parameters to a simple "reverse_proxy" command
+        appended after extraConfig.
+      '';
+    };
+
+    subDomains = mkOption {
+      type = types.attrsOf (types.submodule (import ./sub-domain-options.nix {inherit cfg;}));
+      default = {};
+      example = literalExpression ''
+        {
+          headscale = {
+            reverseProxy = "localhost:8080";
+            extraConfig = '''
+              encode gzip
+            ''';
+          }
+        };
+      '';
+      description = ''
+        Declarative specification of a virtual hosts subdomain served by Caddy.
+      '';
+    };
+
+    subDirectories = mkOption {
+      type = types.attrsOf (types.submodule (import ./sub-dir-options.nix {inherit cfg;}));
+      default = {};
+      example = literalExpression ''
+        {
+          headscale = {
+            appSupport = false;
+            reverseProxy = "localhost:8080";
+            extraConfig = '''
+              encode gzip
+            ''';
+          };
+        };
+      '';
+      description = ''
+        Declarative specification of a subdomain's subdirectories served by Caddy.
+      '';
+    };
+  };
+}
diff --git a/modules/default.nix b/modules/default.nix
index 7f9cf94b..cc709afe 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -2,6 +2,7 @@ self: {
   base = import ./base self;
   base-droid = import ./base/default-droid.nix self;
   borgbackup = import ./borgbackup;
+  caddy-plus = import ./caddy-plus self;
   desktop = import ./desktop self;
   docker = import ./docker self.inputs.khepri;
   esphome-plus = import ./esphome-plus;