From 4f6ea4eb8c1a65c4fcfae486b0ef74d581f5a503 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 4 Aug 2024 23:39:00 +0200 Subject: [PATCH 1/3] cluster/services/incandescence: init --- cluster/services/incandescence/default.nix | 20 +++ cluster/services/incandescence/options.nix | 22 +++ .../incandescence/provider-options.nix | 72 ++++++++++ cluster/services/incandescence/provider.nix | 134 ++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 cluster/services/incandescence/default.nix create mode 100644 cluster/services/incandescence/options.nix create mode 100644 cluster/services/incandescence/provider-options.nix create mode 100644 cluster/services/incandescence/provider.nix diff --git a/cluster/services/incandescence/default.nix b/cluster/services/incandescence/default.nix new file mode 100644 index 0000000..d40c8de --- /dev/null +++ b/cluster/services/incandescence/default.nix @@ -0,0 +1,20 @@ +{ config, ... }: + +{ + imports = [ + ./options.nix + ]; + + services.incandescence = { + nodes = { + provider = config.services.consul.nodes.agent; + }; + nixos = { + provider = [ + ./provider.nix + ./provider-options.nix + ]; + }; + simulacrum.deps = [ "consul" ]; + }; +} diff --git a/cluster/services/incandescence/options.nix b/cluster/services/incandescence/options.nix new file mode 100644 index 0000000..99ab4ad --- /dev/null +++ b/cluster/services/incandescence/options.nix @@ -0,0 +1,22 @@ +{ lib, ... }: + +let + inherit (lib) mkOption; + inherit (lib.types) attrsOf listOf submodule str; +in + +{ + options.incandescence = { + providers = mkOption { + type = attrsOf (submodule ({ name, ... }: { + options = { + objects = mkOption { + type = attrsOf (listOf str); + default = { }; + }; + }; + })); + default = { }; + }; + }; +} diff --git a/cluster/services/incandescence/provider-options.nix b/cluster/services/incandescence/provider-options.nix new file mode 100644 index 0000000..9cc652d --- /dev/null +++ b/cluster/services/incandescence/provider-options.nix @@ -0,0 +1,72 @@ +{ lib, ... }: + +let + inherit (lib) mkEnableOption mkOption; + inherit (lib.types) attrsOf functionTo ints listOf nullOr package submodule str; +in + +{ + options.services.incandescence = { + providers = mkOption { + type = attrsOf (submodule ({ name, ... }: { + options = { + locksmith = mkEnableOption "Locksmith integration"; + + wantedBy = mkOption { + type = listOf str; + }; + + partOf = mkOption { + type = listOf str; + }; + + wants = mkOption { + type = listOf str; + default = [ ]; + }; + + after = mkOption { + type = listOf str; + default = [ ]; + }; + + packages = mkOption { + type = listOf package; + default = [ ]; + }; + + formulae = mkOption { + type = attrsOf (submodule ({ ... }: { + options = { + deps = mkOption { + type = listOf str; + default = [ ]; + }; + + create = mkOption { + type = functionTo str; + }; + + change = mkOption { + type = nullOr (functionTo str); + default = null; + }; + + destroy = mkOption { + type = str; + }; + + destroyAfterDays = mkOption { + type = ints.unsigned; + default = 0; + }; + }; + })); + default = { }; + }; + }; + })); + default = { }; + }; + }; +} diff --git a/cluster/services/incandescence/provider.nix b/cluster/services/incandescence/provider.nix new file mode 100644 index 0000000..6a9993b --- /dev/null +++ b/cluster/services/incandescence/provider.nix @@ -0,0 +1,134 @@ +{ cluster, config, lib, ... }: + +let + inherit (lib) concatStringsSep escapeShellArg flatten filter filterAttrs length mapAttrs mapAttrs' mapAttrsToList mkIf mkMerge pipe stringToCharacters; + + cfg = config.services.incandescence; + clusterCfg = cluster.config.incandescence; +in + +{ + systemd.services = pipe cfg.providers [ + (mapAttrsToList (provider: providerConfig: pipe providerConfig.formulae [ + (mapAttrsToList (formula: formulaConfig: let + kvRoot = "services/incandescence/providers/${provider}/formulae/${formula}"; + time = "$(date +%s)"; + in { + "ignite-${provider}-${formula}-create" = { + description = "Ignite Creation: ${provider} - ${formula}"; + wantedBy = [ "incandescence-${provider}.target" ]; + before = [ "incandescence-${provider}.target" ]; + wants = providerConfig.wants ++ map (dep: "ignite-${provider}-${dep}-create.service") formulaConfig.deps; + after = providerConfig.after ++ map (dep: "ignite-${provider}-${dep}-create.service") formulaConfig.deps; + serviceConfig.Type = "oneshot"; + distributed.enable = true; + path = [ config.services.consul.package ] ++ providerConfig.packages; + script = pipe clusterCfg.providers.${provider}.objects.${formula} [ + (map (object: '' + if ! consul kv get ${kvRoot}/${object}/alive >/dev/null; then + echo "Create ${formula}: ${object}" + if ( + ${formulaConfig.create object} + ) + then + consul kv put ${kvRoot}/${object}/alive true + consul kv delete ${kvRoot}/${object}/destroyOn + else + echo "Creation failed: ${object}" + fi + fi + '')) + (concatStringsSep "\n") + (script: if script == "" then '' + echo "Nothing to create" + '' else script) + ]; + }; + "ignite-${provider}-${formula}-change" = mkIf (formulaConfig.change != null) { + description = "Ignite Change: ${provider} - ${formula}"; + wantedBy = [ "incandescence-${provider}.target" ]; + before = [ "incandescence-${provider}.target" ]; + wants = providerConfig.wants ++ [ "ignite-${provider}-${formula}-create.service" ] ++ map (dep: "ignite-${provider}-${dep}-change.service") formulaConfig.deps; + after = providerConfig.after ++ [ "ignite-${provider}-${formula}-create.service" ] ++ map (dep: "ignite-${provider}-${dep}-change.service") formulaConfig.deps; + serviceConfig.Type = "oneshot"; + distributed.enable = true; + path = [ config.services.consul.package ] ++ providerConfig.packages; + script = pipe clusterCfg.providers.${provider}.objects.${formula} [ + (map (object: '' + echo "Change ${formula}: ${object}" + ( + ${formulaConfig.change object} + ) || echo "Change failed: ${object}" + '')) + (concatStringsSep "\n") + (script: if script == "" then '' + echo "Nothing to change" + '' else script) + ]; + }; + "ignite-${provider}-${formula}-destroy" = { + description = "Ignite Destruction: ${provider} - ${formula}"; + wantedBy = [ "incandescence-${provider}.target" ] ++ map (dep: "ignite-${provider}-${dep}-destroy.service") formulaConfig.deps; + before = [ "incandescence-${provider}.target" ] ++ map (dep: "ignite-${provider}-${dep}-destroy.service") formulaConfig.deps; + wants = providerConfig.wants ++ [ "ignite-${provider}-${formula}-change.service" ]; + after = providerConfig.after ++ [ "ignite-${provider}-${formula}-change.service" ]; + serviceConfig.Type = "oneshot"; + distributed.enable = true; + path = [ config.services.consul.package ] ++ providerConfig.packages; + script = let + fieldNum = pipe kvRoot [ + stringToCharacters + (filter (x: x == "/")) + length + (builtins.add 2) + toString + ]; + keyFilter = pipe clusterCfg.providers.${provider}.objects.${formula} [ + (map (x: escapeShellArg "^${x}$")) + (concatStringsSep " \\\n -e ") + ]; + destroyAfterDays = toString formulaConfig.destroyAfterDays; + in '' + consul kv get --keys ${kvRoot}/ | cut -d/ -f${fieldNum} | grep -v -e ${keyFilter} | while read object; do + if consul kv get ${kvRoot}/$object/alive >/dev/null; then + destroyOn="$(consul kv get ${kvRoot}/$object/destroyOn || true)" + if [[ -z "$destroyOn" && "${destroyAfterDays}" -ne 0 ]]; then + echo "Schedule ${formula} for destruction in ${destroyAfterDays} days: $object" + consul kv put ${kvRoot}/$object/destroyOn "$((${time} + 86400 * ${destroyAfterDays}))" + elif [[ "${destroyAfterDays}" -eq 0 || "${time}" -ge "$destroyOn" ]]; then + echo "Destroy ${formula}: $object" + export OBJECT="$object" + if ( + ${formulaConfig.destroy} + ) + then + consul kv delete --recurse ${kvRoot}/$object + else + echo "Destruction failed: $object" + fi + else + echo "Scheduled for destruction on $destroyOn (now: ${time})" + fi + fi + done + ''; + }; + })) + ])) + flatten + mkMerge + ]; + + systemd.targets = mapAttrs' (provider: providerConfig: { + name = "incandescence-${provider}"; + value = { + description = "An Incandescence | ${provider}"; + inherit (providerConfig) wantedBy partOf; + }; + }) cfg.providers; + + services.locksmith.providers = mapAttrs (provider: providerConfig: { + wantedBy = [ "incandescence-${provider}.target" ]; + after = [ "incandescence-${provider}.target" ]; + }) (filterAttrs (_: providerConfig: providerConfig.locksmith) cfg.providers); +} -- 2.47.0 From d1c0e9d7f9e4bd378fb3c8cc879db518bfafef53 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 12 Aug 2024 01:48:00 +0200 Subject: [PATCH 2/3] cluster/services/incandescence: add base layout for ascensions --- cluster/services/incandescence/provider.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cluster/services/incandescence/provider.nix b/cluster/services/incandescence/provider.nix index 6a9993b..860a2d1 100644 --- a/cluster/services/incandescence/provider.nix +++ b/cluster/services/incandescence/provider.nix @@ -131,4 +131,14 @@ in wantedBy = [ "incandescence-${provider}.target" ]; after = [ "incandescence-${provider}.target" ]; }) (filterAttrs (_: providerConfig: providerConfig.locksmith) cfg.providers); + + system.ascensions = mapAttrs' (provider: providerConfig: { + name = "incandescence-${provider}"; + value = { + distributed = true; + requiredBy = map (formula: "ignite-${provider}-${formula}-create.service") (lib.attrNames providerConfig.formulae); + before = map (formula: "ignite-${provider}-${formula}-create.service") (lib.attrNames providerConfig.formulae); + incantations = lib.mkDefault (i: []); + }; + }) cfg.providers; } -- 2.47.0 From d015c77ffaf2223b1ce8ae20ac0a6d62e97549d5 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 14 Aug 2024 16:00:35 +0200 Subject: [PATCH 3/3] cluster/services/incandescence: test in simulacrum --- cluster/services/incandescence/default.nix | 7 ++- .../incandescence/simulacrum/test-data.nix | 8 ++++ .../incandescence/simulacrum/test.nix | 47 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 cluster/services/incandescence/simulacrum/test-data.nix create mode 100644 cluster/services/incandescence/simulacrum/test.nix diff --git a/cluster/services/incandescence/default.nix b/cluster/services/incandescence/default.nix index d40c8de..1e93a32 100644 --- a/cluster/services/incandescence/default.nix +++ b/cluster/services/incandescence/default.nix @@ -3,6 +3,7 @@ { imports = [ ./options.nix + ./simulacrum/test-data.nix ]; services.incandescence = { @@ -15,6 +16,10 @@ ./provider-options.nix ]; }; - simulacrum.deps = [ "consul" ]; + simulacrum = { + enable = true; + deps = [ "consul" "locksmith" ]; + settings = ./simulacrum/test.nix; + }; }; } diff --git a/cluster/services/incandescence/simulacrum/test-data.nix b/cluster/services/incandescence/simulacrum/test-data.nix new file mode 100644 index 0000000..d36e1e5 --- /dev/null +++ b/cluster/services/incandescence/simulacrum/test-data.nix @@ -0,0 +1,8 @@ +{ config, lib, ... }: +{ + incandescence = lib.mkIf config.simulacrum { + providers = config.lib.forService "incandescence" { + test.objects.example = [ "example1" "example2" ]; + }; + }; +} diff --git a/cluster/services/incandescence/simulacrum/test.nix b/cluster/services/incandescence/simulacrum/test.nix new file mode 100644 index 0000000..1a9bf98 --- /dev/null +++ b/cluster/services/incandescence/simulacrum/test.nix @@ -0,0 +1,47 @@ +{ cluster, lib, ... }: + +let + providers = lib.take 2 cluster.config.services.incandescence.nodes.provider; +in + +{ + nodes = lib.genAttrs providers (lib.const { + services.incandescence.providers.test = { + wantedBy = [ "multi-user.target" ]; + partOf = [ ]; + formulae.example = { + create = x: "consul kv put testData/${x} ${x}"; + destroy = "consul kv delete testData/$OBJECT"; + }; + }; + }); + + testScript = '' + import json + nodeNames = json.loads('${builtins.toJSON providers}') + nodes = [ n for n in machines if n.name in nodeNames ] + + start_all() + + consulConfig = json.loads(nodes[0].succeed("cat /etc/consul.json")) + addr = consulConfig["addresses"]["http"] + port = consulConfig["ports"]["http"] + setEnv = f"CONSUL_HTTP_ADDR={addr}:{port}" + + with subtest("should create objects"): + for node in nodes: + node.wait_for_unit("incandescence-test.target") + nodes[0].succeed(f"[[ $({setEnv} consul kv get testData/example1) == example1 ]]") + nodes[0].succeed(f"[[ $({setEnv} consul kv get testData/example2) == example2 ]]") + + with subtest("should destroy objects"): + nodes[0].succeed(f"{setEnv} consul kv put testData/example3 example3") + nodes[0].succeed(f"{setEnv} consul kv put services/incandescence/providers/test/formulae/example/example3/alive true") + nodes[1].succeed(f"{setEnv} consul kv get testData/example3") + for node in nodes: + node.systemctl("isolate default") + for node in nodes: + node.wait_for_unit("incandescence-test.target") + nodes[0].fail(f"{setEnv} consul kv get testData/example3") + ''; +} -- 2.47.0