diff --git a/modules/ascensions/default.nix b/modules/ascensions/default.nix new file mode 100644 index 0000000..4fe9dd8 --- /dev/null +++ b/modules/ascensions/default.nix @@ -0,0 +1,125 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + ascensionsDir = "/var/lib/ascensions"; + + ascensionType = { name, ... }: { + options = { + incantations = mkOption { + type = with types; functionTo (listOf package); + default = []; + }; + distributed = mkOption { + type = types.bool; + default = false; + description = "Whether to perform a distributed ascension using Consul KV."; + }; + requiredBy = mkOption { + type = with types; listOf str; + default = [ "${name}.service" ]; + description = "Services that require this ascension."; + }; + before = mkOption { + type = with types; listOf str; + default = []; + description = "Run the ascension before these services."; + }; + after = mkOption { + type = with types; listOf str; + default = []; + description = "Run the ascension after these services."; + }; + }; + }; + + runIncantations = f: with pkgs; f rec { + execShellWith = extraPackages: script: writeShellScript "incantation" '' + export PATH='${makeBinPath ([ coreutils ] ++ extraPackages)}' + ${script} + ''; + + execShell = execShellWith []; + + multiple = incantations: execShell (concatStringsSep " && " incantations); + + move = from: to: execShell '' + test -e ${escapeShellArg from} && rm -rf ${escapeShellArg to} + mkdir -p "$(dirname ${escapeShellArg to})" + mv ${escapeShellArgs [ from to ]} + ''; + + chmod = mode: target: "chmod -R ${escapeShellArgs [ mode target ]}"; + }; + + consul = config.services.consul.package; +in + +{ + options.system.ascensions = mkOption { + type = with types; attrsOf (submodule ascensionType); + default = {}; + }; + + config = { + systemd = { + tmpfiles.rules = [ + "d ${ascensionsDir} 0755 root root - -" + ]; + + services = mapAttrs' (name: asc: { + name = "ascend-${name}"; + value = let + incantations = runIncantations asc.incantations; + targetLevel = toString (length incantations); + in { + description = "Ascension for ${name}"; + inherit (asc) requiredBy before; + after = asc.after ++ (lib.optional asc.distributed "consul.service"); + serviceConfig.Type = "oneshot"; + distributed.enable = asc.distributed; + script = '' + incantations=(${concatStringsSep " " incantations}) + ${if asc.distributed then /*bash*/ '' + getLevel() { + ${consul}/bin/consul kv get 'ascensions/${name}/currentLevel' + } + setLevel() { + ${consul}/bin/consul kv put 'ascensions/${name}/currentLevel' "$1" >/dev/null + } + isEmpty() { + ! getLevel >/dev/null 2>/dev/null + } + '' else /*bash*/ '' + levelFile='${ascensionsDir}/${name}' + getLevel() { + echo "$(<"$levelFile")" + } + setLevel() { + echo "$1" > "$levelFile" + } + isEmpty() { + [[ ! -e "$levelFile" ]] + } + '' + } + if isEmpty; then + setLevel '${targetLevel}' + echo Initializing at level ${targetLevel} + exit 0 + fi + cur=$(getLevel) + echo Current level: $cur + for lvl in $(seq $(($cur+1)) ${targetLevel}); do + echo Running incantation for level $lvl... + ''${incantations[$((lvl-1))]} + setLevel "$lvl" + done + echo All incantations complete, ascended to level $(getLevel) + ''; + }; + }) config.system.ascensions; + }; + }; +} diff --git a/modules/part.nix b/modules/part.nix index 00159ad..7546353 100644 --- a/modules/part.nix +++ b/modules/part.nix @@ -7,6 +7,7 @@ in { flake.nixosModules = with config.flake.nixosModules; { autopatch = ./autopatch; + ascensions = ./ascensions; consul-distributed-services = ./consul-distributed-services; consul-service-registry = ./consul-service-registry; deploy-rs-receiver = ./deploy-rs-receiver; @@ -45,6 +46,7 @@ in serverBase = group [ machineBase + ascensions consul-distributed-services consul-service-registry deploy-rs-receiver