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, +) => `# 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; + + // 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 Binary files a/flake.lock and b/flake.lock differ diff --git a/flake.nix b/flake.nix index db600794..e01d18e8 100644 Binary files a/flake.nix and b/flake.nix differ 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;