depot/modules/ascensions/default.nix

138 lines
4.1 KiB
Nix

{ 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.";
};
};
};
builtinIncantations = with pkgs; 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 ]}";
};
allIncantations = builtinIncantations // mapAttrs (_: mk: mk allIncantations) config.system.extraIncantations;
runIncantations = f: f allIncantations;
consul = config.services.consul.package;
in
{
options.system = {
ascensions = mkOption {
type = with types; attrsOf (submodule ascensionType);
default = {};
};
extraIncantations = mkOption {
type = with types; attrsOf (functionTo raw);
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}";
wantedBy = [ "multi-user.target" ];
inherit (asc) requiredBy before;
after = asc.after ++ (lib.optional asc.distributed "consul-ready.service");
requires = lib.optional asc.distributed "consul-ready.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;
};
};
}