diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 7bc68bf5..94f81623 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -56,3 +56,47 @@ jobs: steps: - uses: actions/checkout@v4 - run: nix build .#checks.x86_64-linux.deployment-panel -L + + check-deployment-model-ssh: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: nix build .#checks.x86_64-linux.deployment-model-ssh -L + + check-deployment-model-nixops4: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: nix build .#checks.x86_64-linux.deployment-model-nixops4 -L + + check-deployment-model-tf: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: nix build .#checks.x86_64-linux.deployment-model-tf -L + + ## NOTE: NixOps4 does not provide a good “dry run” mode, so we instead check + ## proxies for resources, namely whether their `.#vmOptions.` and + ## `.#nixosConfigurations.` outputs evaluate and build correctly, and + ## whether we can dry run `infra/proxmox-*.sh` on them. This will not catch + ## everything, and in particular not issues in how NixOps4 wires up the + ## resources, but that is still something. + check-resources: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: | + set -euC + echo ==================== [ VM Options ] ==================== + machines=$(nix eval --impure --raw --expr 'with builtins; toString (attrNames (getFlake (toString ./.)).vmOptions)') + for machine in $machines; do + echo ~~~~~~~~~~~~~~~~~~~~~: $machine :~~~~~~~~~~~~~~~~~~~~~ + nix build .#checks.x86_64-linux.vmOptions-$machine + done + echo + echo ==================== [ NixOS Configurations ] ==================== + machines=$(nix eval --impure --raw --expr 'with builtins; toString (attrNames (getFlake (toString ./.)).nixosConfigurations)') + for machine in $machines; do + echo ~~~~~~~~~~~~~~~~~~~~~: $machine :~~~~~~~~~~~~~~~~~~~~~ + nix build .#checks.x86_64-linux.nixosConfigurations-$machine + done diff --git a/.forgejo/workflows/update.yaml b/.forgejo/workflows/update.yaml index 7304fd7b..19dacf15 100644 --- a/.forgejo/workflows/update.yaml +++ b/.forgejo/workflows/update.yaml @@ -13,7 +13,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - name: Update pins - run: nix-shell --run "npins update" + run: nix-shell --run "npins --verbose update" - name: Create PR uses: https://github.com/KiaraGrouwstra/gitea-create-pull-request@f9f80aa5134bc5c03c38f5aaa95053492885b397 with: diff --git a/default.nix b/default.nix index 24b73cd2..e929e516 100644 --- a/default.nix +++ b/default.nix @@ -11,7 +11,8 @@ let ; inherit (pkgs) lib; inherit (import sources.flake-inputs) import-flake; - inherit ((import-flake { src = ./.; }).inputs) nixops4; + inputs = (import-flake { src = ./.; }).inputs; + inherit (inputs) nixops4; panel = import ./panel { inherit sources system; }; pre-commit-check = (import "${git-hooks}/nix" { @@ -78,6 +79,7 @@ in # re-export inputs so they can be overridden granularly # (they can't be accessed from the outside any other way) inherit + inputs sources system pkgs diff --git a/deployment/check/basic/constants.nix b/deployment/check/basic/constants.nix index 3cf28d8f..f2573fb8 100644 --- a/deployment/check/basic/constants.nix +++ b/deployment/check/basic/constants.nix @@ -5,4 +5,5 @@ ]; pathToRoot = ../../..; pathFromRoot = ./.; + useFlake = true; } diff --git a/deployment/check/basic/default.nix b/deployment/check/basic/default.nix index 6479b6be..176defce 100644 --- a/deployment/check/basic/default.nix +++ b/deployment/check/basic/default.nix @@ -10,5 +10,10 @@ runNixOSTest { ./nixosTest.nix ]; _module.args = { inherit inputs sources; }; - inherit (import ./constants.nix) targetMachines pathToRoot pathFromRoot; + inherit (import ./constants.nix) + targetMachines + pathToRoot + pathFromRoot + useFlake + ; } diff --git a/deployment/check/basic/nixosTest.nix b/deployment/check/basic/nixosTest.nix index 93c8ad23..10ed196f 100644 --- a/deployment/check/basic/nixosTest.nix +++ b/deployment/check/basic/nixosTest.nix @@ -1,4 +1,9 @@ -{ inputs, lib, ... }: +{ + inputs, + lib, + config, + ... +}: { _class = "nixosTest"; @@ -8,6 +13,7 @@ sourceFileset = lib.fileset.unions [ ./constants.nix ./deployment.nix + (config.pathToCwd + "/flake-under-test.nix") ]; nodes.deployer = diff --git a/deployment/check/cli/constants.nix b/deployment/check/cli/constants.nix index ce3b5e4d..a48d9076 100644 --- a/deployment/check/cli/constants.nix +++ b/deployment/check/cli/constants.nix @@ -8,4 +8,5 @@ pathToRoot = ../../..; pathFromRoot = ./.; enableAcme = true; + useFlake = true; } diff --git a/deployment/check/cli/default.nix b/deployment/check/cli/default.nix index 03c1379f..64448280 100644 --- a/deployment/check/cli/default.nix +++ b/deployment/check/cli/default.nix @@ -15,5 +15,6 @@ runNixOSTest { pathToRoot pathFromRoot enableAcme + useFlake ; } diff --git a/deployment/check/cli/nixosTest.nix b/deployment/check/cli/nixosTest.nix index d171a182..f7f1cf2d 100644 --- a/deployment/check/cli/nixosTest.nix +++ b/deployment/check/cli/nixosTest.nix @@ -1,6 +1,7 @@ { inputs, hostPkgs, + config, lib, ... }: @@ -19,6 +20,7 @@ in sourceFileset = lib.fileset.unions [ ./constants.nix ./deployments.nix + (config.pathToCwd + "/flake-under-test.nix") # REVIEW: I would like to be able to grab all of `/deployment` minus # `/deployment/check`, but I can't because there is a bunch of other files diff --git a/deployment/check/common/data-model-options.nix b/deployment/check/common/data-model-options.nix new file mode 100644 index 00000000..8492bee3 --- /dev/null +++ b/deployment/check/common/data-model-options.nix @@ -0,0 +1,15 @@ +{ + lib, + ... +}: +let + inherit (lib) types; +in +{ + options = { + host = lib.mkOption { + type = types.str; + description = "name of the host to deploy to"; + }; + }; +} diff --git a/deployment/check/common/data-model.nix b/deployment/check/common/data-model.nix new file mode 100644 index 00000000..8928e93b --- /dev/null +++ b/deployment/check/common/data-model.nix @@ -0,0 +1,214 @@ +{ + config, + system, + inputs ? (import ../../../default.nix { }).inputs, + sources ? import ../../../npins, + ... +}: + +let + inherit (sources) nixpkgs; + pkgs = import nixpkgs { inherit system; }; + inherit (pkgs) lib; + deployment-config = config; + inherit (deployment-config) nodeName; + inherit (lib) mkOption types; + eval = + module: + (lib.evalModules { + specialArgs = { + inherit pkgs inputs; + }; + modules = [ + module + ../../data-model.nix + ]; + }).config; + fediversity = eval ( + { config, ... }: + { + config = { + resources.login-shell = { + description = "The operator needs to be able to log into the shell"; + request = + { ... }: + { + _class = "fediversity-resource-request"; + options = { + wheel = mkOption { + description = "Whether the login user needs root permissions"; + type = types.bool; + default = false; + }; + packages = mkOption { + description = "Packages that need to be available in the user environment"; + type = with types; attrsOf package; + }; + }; + }; + policy = + { config, ... }: + { + _class = "fediversity-resource-policy"; + options = { + username = mkOption { + description = "Username for the operator"; + type = types.str; # TODO: use the proper constraints from NixOS + }; + wheel = mkOption { + description = "Whether to allow login with root permissions"; + type = types.bool; + default = false; + }; + }; + config = { + resource-type = types.raw; # TODO: splice out the user type from NixOS + apply = + requests: + let + # Filter out requests that need wheel if policy doesn't allow it + validRequests = lib.filterAttrs ( + _name: req: !req.login-shell.wheel || config.wheel + ) requests.resources; + in + lib.optionalAttrs (validRequests != { }) { + ${config.username} = { + isNormalUser = true; + packages = + with lib; + attrValues (concatMapAttrs (_name: request: request.login-shell.packages) validRequests); + extraGroups = lib.optional config.wheel "wheel"; + }; + }; + }; + }; + }; + applications.hello = + { ... }: + { + description = ''Command-line tool that will print "Hello, world!" on the terminal''; + module = + { ... }: + { + options.enable = lib.mkEnableOption "Hello in the shell"; + }; + implementation = cfg: { + input = cfg; + output.resources = lib.optionalAttrs cfg.enable { + hello.login-shell.packages.hello = pkgs.hello; + }; + }; + }; + environments = + let + mkNixosConfiguration = + environment: requests: + { ... }: + { + imports = [ + ./data-model-options.nix + ../common/sharedOptions.nix + ../common/targetNode.nix + "${nixpkgs}/nixos/modules/profiles/qemu-guest.nix" + ]; + + users.users = environment.config.resources."operator-environment".login-shell.apply { + resources = lib.filterAttrs (_name: value: value ? login-shell) ( + lib.concatMapAttrs ( + k': req: lib.mapAttrs' (k: lib.nameValuePair "${k'}.${k}") req.resources + ) requests + ); + }; + }; + in + { + single-nixos-vm-ssh = environment: { + resources."operator-environment".login-shell.username = "operator"; + implementation = requests: { + input = requests; + output.ssh-host = { + nixos-configuration = mkNixosConfiguration environment requests; + ssh = { + username = "root"; + host = nodeName; + key-file = null; + }; + }; + }; + }; + single-nixos-vm-nixops4 = environment: { + resources."operator-environment".login-shell.username = "operator"; + implementation = requests: { + input = requests; + output.nixops4 = + { providers, ... }: + { + providers = { + inherit (inputs.nixops4.modules.nixops4Provider) local; + }; + resources.${nodeName} = { + type = providers.local.exec; + imports = [ + inputs.nixops4-nixos.modules.nixops4Resource.nixos + ../common/targetResource.nix + ]; + nixos.module = mkNixosConfiguration environment requests; + _module.args = { inherit inputs sources; }; + inherit (deployment-config) nodeName pathToRoot pathFromRoot; + }; + }; + }; + }; + single-nixos-vm-tf = environment: { + resources."operator-environment".login-shell.username = "operator"; + implementation = requests: { + input = requests; + output.tf-host = { + nixos-configuration = mkNixosConfiguration environment requests; + ssh = { + username = "root"; + host = nodeName; + key-file = null; + }; + }; + }; + }; + }; + }; + options = { + "example-configuration" = mkOption { + type = config.configuration; + default = { + enable = true; + applications.hello.enable = true; + }; + }; + "ssh-deployment" = + let + env = config.environments."single-nixos-vm-ssh"; + in + mkOption { + type = env.resource-mapping.output-type; + default = env.deployment config."example-configuration"; + }; + "nixops4-deployment" = + let + env = config.environments."single-nixos-vm-nixops4"; + in + mkOption { + type = env.resource-mapping.output-type; + default = env.deployment config."example-configuration"; + }; + "tf-deployment" = + let + env = config.environments."single-nixos-vm-tf"; + in + mkOption { + type = env.resource-mapping.output-type; + default = env.deployment config."example-configuration"; + }; + }; + } + ); +in +fediversity diff --git a/deployment/check/common/deployerNode.nix b/deployment/check/common/deployerNode.nix index 987a0a7c..dcb5deef 100644 --- a/deployment/check/common/deployerNode.nix +++ b/deployment/check/common/deployerNode.nix @@ -59,6 +59,7 @@ in inputs.nixpkgs sources.flake-parts + sources.nixpkgs sources.flake-inputs sources.git-hooks diff --git a/deployment/check/common/nixosTest.nix b/deployment/check/common/nixosTest.nix index cb52ed9f..93bd3fef 100644 --- a/deployment/check/common/nixosTest.nix +++ b/deployment/check/common/nixosTest.nix @@ -48,7 +48,8 @@ in extraTestScript = mkOption { }; sourceFileset = mkOption { - ## REVIEW: Upstream to nixpkgs? + ## FIXME: grab `lib.types.fileset` from NixOS, once upstreaming PR + ## https://github.com/NixOS/nixpkgs/pull/428293 lands. type = types.mkOptionType { name = "fileset"; description = "fileset"; @@ -75,8 +76,6 @@ in ./sharedOptions.nix ./targetNode.nix ./targetResource.nix - - (config.pathToCwd + "/flake-under-test.nix") ]; acmeNodeIP = config.nodes.acme.networking.primaryIPAddress; @@ -163,31 +162,38 @@ in deployer.succeed(f"echo '{host_key}' > ${config.pathFromRoot}/${tm}_host_key.pub") '')} - ## NOTE: This is super slow. It could probably be optimised in Nix, for - ## instance by allowing to grab things directly from the host's store. - ## - ## NOTE: We use the repository as-is (cf `src` above), overriding only - ## `flake.nix` by our `flake-under-test.nix`. We also override the flake - ## lock file to use locally available inputs, as we cannot download them. - ## - with subtest("Override the flake and its lock"): - deployer.succeed("cp ${config.pathFromRoot}/flake-under-test.nix flake.nix") - deployer.succeed(""" - nix flake lock --extra-experimental-features 'flakes nix-command' \ - --offline -v \ - --override-input nixops4 ${inputs.nixops4.packages.${system}.flake-in-a-bottle} \ - \ - --override-input nixops4-nixos ${inputs.nixops4-nixos} \ - --override-input nixops4-nixos/flake-parts ${inputs.nixops4-nixos.inputs.flake-parts} \ - --override-input nixops4-nixos/flake-parts/nixpkgs-lib ${inputs.nixops4-nixos.inputs.flake-parts.inputs.nixpkgs-lib} \ - --override-input nixops4-nixos/nixops4-nixos ${emptyFlake} \ - --override-input nixops4-nixos/nixpkgs ${inputs.nixops4-nixos.inputs.nixpkgs} \ - --override-input nixops4-nixos/nixops4 ${ - inputs.nixops4-nixos.inputs.nixops4.packages.${system}.flake-in-a-bottle - } \ - --override-input nixops4-nixos/git-hooks-nix ${emptyFlake} \ - ; - """) + ${ + if config.useFlake then + '' + ## NOTE: This is super slow. It could probably be optimised in Nix, for + ## instance by allowing to grab things directly from the host's store. + ## + ## NOTE: We use the repository as-is (cf `src` above), overriding only + ## `flake.nix` by our `flake-under-test.nix`. We also override the flake + ## lock file to use locally available inputs, as we cannot download them. + ## + with subtest("Override the flake and its lock"): + deployer.succeed("cp ${config.pathFromRoot}/flake-under-test.nix flake.nix") + deployer.succeed(""" + nix flake lock --extra-experimental-features 'flakes nix-command' \ + --offline -v \ + --override-input nixops4 ${inputs.nixops4.packages.${system}.flake-in-a-bottle} \ + \ + --override-input nixops4-nixos ${inputs.nixops4-nixos} \ + --override-input nixops4-nixos/flake-parts ${inputs.nixops4-nixos.inputs.flake-parts} \ + --override-input nixops4-nixos/flake-parts/nixpkgs-lib ${inputs.nixops4-nixos.inputs.flake-parts.inputs.nixpkgs-lib} \ + --override-input nixops4-nixos/nixops4-nixos ${emptyFlake} \ + --override-input nixops4-nixos/nixpkgs ${inputs.nixops4-nixos.inputs.nixpkgs} \ + --override-input nixops4-nixos/nixops4 ${ + inputs.nixops4-nixos.inputs.nixops4.packages.${system}.flake-in-a-bottle + } \ + --override-input nixops4-nixos/git-hooks-nix ${emptyFlake} \ + ; + """) + '' + else + "" + } ${optionalString config.enableAcme '' with subtest("Set up handmade DNS"): diff --git a/deployment/check/common/sharedOptions.nix b/deployment/check/common/sharedOptions.nix index 7201a8f5..c0efc6cf 100644 --- a/deployment/check/common/sharedOptions.nix +++ b/deployment/check/common/sharedOptions.nix @@ -64,5 +64,7 @@ in during the test to the correct value. ''; }; + + useFlake = lib.mkEnableOption "Use a flake in the test."; }; } diff --git a/deployment/check/common/targetNode.nix b/deployment/check/common/targetNode.nix index a346d873..e88be811 100644 --- a/deployment/check/common/targetNode.nix +++ b/deployment/check/common/targetNode.nix @@ -28,6 +28,8 @@ in system.switch.enable = true; nix = { + # short-cut network time-outs + settings.download-attempts = 1; ## Not used; save a large copy operation channel.enable = false; registry = lib.mkForce { }; diff --git a/deployment/check/data-model-nixops4/constants.nix b/deployment/check/data-model-nixops4/constants.nix new file mode 100644 index 00000000..548d6605 --- /dev/null +++ b/deployment/check/data-model-nixops4/constants.nix @@ -0,0 +1,9 @@ +{ + targetMachines = [ + "nixops4" + ]; + pathToRoot = ../../..; + pathFromRoot = ./.; + enableAcme = true; + useFlake = true; +} diff --git a/deployment/check/data-model-nixops4/default.nix b/deployment/check/data-model-nixops4/default.nix new file mode 100644 index 00000000..b735cd53 --- /dev/null +++ b/deployment/check/data-model-nixops4/default.nix @@ -0,0 +1,22 @@ +{ + runNixOSTest, + inputs, + sources, +}: + +runNixOSTest { + imports = [ + ../../data-model.nix + ../../function.nix + ../common/nixosTest.nix + ./nixosTest.nix + ]; + _module.args = { inherit inputs sources; }; + inherit (import ./constants.nix) + targetMachines + pathToRoot + pathFromRoot + enableAcme + useFlake + ; +} diff --git a/deployment/check/data-model-nixops4/flake-under-test.nix b/deployment/check/data-model-nixops4/flake-under-test.nix new file mode 100644 index 00000000..6a4ce06c --- /dev/null +++ b/deployment/check/data-model-nixops4/flake-under-test.nix @@ -0,0 +1,29 @@ +{ + inputs = { + nixops4.follows = "nixops4-nixos/nixops4"; + nixops4-nixos.url = "github:nixops4/nixops4-nixos"; + }; + + outputs = + inputs: + import ./mkFlake.nix inputs ( + { inputs, ... }: + let + system = "x86_64-linux"; + in + { + imports = [ + inputs.nixops4.modules.flake.default + ]; + + nixops4Deployments.check-deployment-model = + (import ./deployment/check/common/data-model.nix { + inherit system inputs; + config = { + inherit (import ./deployment/check/data-model-nixops4/constants.nix) pathToRoot pathFromRoot; + nodeName = "nixops4"; + }; + })."nixops4-deployment".nixops4; + } + ); +} diff --git a/deployment/check/data-model-nixops4/nixosTest.nix b/deployment/check/data-model-nixops4/nixosTest.nix new file mode 100644 index 00000000..5a4499f3 --- /dev/null +++ b/deployment/check/data-model-nixops4/nixosTest.nix @@ -0,0 +1,52 @@ +{ + lib, + config, + inputs, + ... +}: +{ + _class = "nixosTest"; + imports = [ + ../common/data-model-options.nix + ]; + + name = "deployment-model"; + sourceFileset = lib.fileset.unions [ + ../../data-model.nix + ../../function.nix + ../common/data-model.nix + ../common/data-model-options.nix + ./constants.nix + (config.pathToCwd + "/flake-under-test.nix") + ]; + + nodes.deployer = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + inputs.nixops4.packages.${system}.default + jq + ]; + + # FIXME: sad times + system.extraDependencies = with pkgs; [ + jq + jq.inputDerivation + ]; + + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + ]; + }; + }; + + extraTestScript = '' + with subtest("nixops4"): + nixops4.fail("hello 1>&2") + deployer.succeed("nixops4 apply check-deployment-model --show-trace --verbose --no-interactive 1>&2") + nixops4.succeed("su - operator -c hello 1>&2") + ''; +} diff --git a/deployment/check/data-model-ssh/constants.nix b/deployment/check/data-model-ssh/constants.nix new file mode 100644 index 00000000..ef212896 --- /dev/null +++ b/deployment/check/data-model-ssh/constants.nix @@ -0,0 +1,8 @@ +{ + targetMachines = [ + "ssh" + ]; + pathToRoot = ../../..; + pathFromRoot = ./.; + enableAcme = true; +} diff --git a/deployment/check/data-model-ssh/default.nix b/deployment/check/data-model-ssh/default.nix new file mode 100644 index 00000000..1815f19a --- /dev/null +++ b/deployment/check/data-model-ssh/default.nix @@ -0,0 +1,21 @@ +{ + runNixOSTest, + inputs, + sources, +}: + +runNixOSTest { + imports = [ + ../../data-model.nix + ../../function.nix + ../common/nixosTest.nix + ./nixosTest.nix + ]; + _module.args = { inherit inputs sources; }; + inherit (import ./constants.nix) + targetMachines + pathToRoot + pathFromRoot + enableAcme + ; +} diff --git a/deployment/check/data-model-ssh/nixosTest.nix b/deployment/check/data-model-ssh/nixosTest.nix new file mode 100644 index 00000000..dbc79fbf --- /dev/null +++ b/deployment/check/data-model-ssh/nixosTest.nix @@ -0,0 +1,110 @@ +{ + lib, + config, + pkgs, + inputs, + ... +}: +let + inherit (import ./constants.nix) pathToRoot pathFromRoot; + inherit (pkgs) system; + escapedJson = v: lib.replaceStrings [ "\"" ] [ "\\\\\"" ] (lib.strings.toJSON v); + deployment-config = { + inherit pathToRoot pathFromRoot; + inherit (config) enableAcme; + acmeNodeIP = if config.enableAcme then config.nodes.acme.networking.primaryIPAddress else null; + nodeName = "ssh"; + }; + inherit + ((import ../common/data-model.nix { + inherit system inputs; + config = deployment-config; + })."ssh-deployment".ssh-host.ssh + ) + host + username + key-file + ; +in +{ + _class = "nixosTest"; + imports = [ + ../common/data-model-options.nix + ]; + + name = "deployment-model"; + sourceFileset = lib.fileset.unions [ + ../../data-model.nix + ../../function.nix + ../common/data-model.nix + ../common/data-model-options.nix + ./constants.nix + ]; + + nodes.deployer = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + jq + ]; + + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + ]; + }; + }; + + extraTestScript = '' + with subtest("ssh: Check the status before deployment"): + ssh.fail("hello 1>&2") + + with subtest("ssh: Run the deployment"): + deployer.succeed(""" + set -euo pipefail + + # INSTANTIATE + command=(nix-instantiate --show-trace --expr ' + let + system = "${pkgs.system}"; # FIXME: what system are we deploying to? + in + import ${pathToRoot}/deployment/nixos.nix { + inherit system; + configuration = ( + import ${pathToRoot}/deployment/check/common/data-model.nix { + inherit system; + config = builtins.fromJSON "${escapedJson deployment-config}"; + } + )."ssh-deployment".ssh-host.nixos-configuration; + } + ') + # DEPLOY + host="${lib.defaultTo "root" username}@${host}" + sshOpts=( + ${if key-file == null then "" else "-i ${key-file}"} + -o StrictHostKeyChecking=no + -o "ConnectTimeout=1" + -o "ServerAliveInterval=1" + ) + # instantiate the config in /nix/store + "''${command[@]}" --show-trace -A out_path + # get the realized derivation to deploy + outPath=$(nix-store --realize "$("''${command[@]}" --show-trace --eval --strict --json | jq -r '.drv_path')") + # deploy the config by nix-copy-closure + NIX_SSHOPTS="''${sshOpts[*]}" nix-copy-closure --to "$host" "$outPath" --gzip --use-substitutes + # switch the remote host to the config + output=$(ssh "''${sshOpts[@]}" "$host" "nix-env --profile /nix/var/nix/profiles/system --set $outPath; nohup $outPath/bin/switch-to-configuration switch &" 2>&1) || echo "status code: $?" + echo "output: $output" + if [[ $output != *"Timeout, server ssh not responding"* ]]; then + echo "non-timeout error: $output" + exit 1 + else + exit 0 + fi + """) + ssh.wait_for_unit("multi-user.target") + ssh.succeed("su - operator -c hello 1>&2") + ''; +} diff --git a/deployment/check/data-model-tf/constants.nix b/deployment/check/data-model-tf/constants.nix new file mode 100644 index 00000000..a3e4e33a --- /dev/null +++ b/deployment/check/data-model-tf/constants.nix @@ -0,0 +1,8 @@ +{ + targetMachines = [ + "target" + ]; + pathToRoot = ../../..; + pathFromRoot = ./.; + enableAcme = true; +} diff --git a/deployment/check/data-model-tf/default.nix b/deployment/check/data-model-tf/default.nix new file mode 100644 index 00000000..1815f19a --- /dev/null +++ b/deployment/check/data-model-tf/default.nix @@ -0,0 +1,21 @@ +{ + runNixOSTest, + inputs, + sources, +}: + +runNixOSTest { + imports = [ + ../../data-model.nix + ../../function.nix + ../common/nixosTest.nix + ./nixosTest.nix + ]; + _module.args = { inherit inputs sources; }; + inherit (import ./constants.nix) + targetMachines + pathToRoot + pathFromRoot + enableAcme + ; +} diff --git a/deployment/check/data-model-tf/deploy.sh b/deployment/check/data-model-tf/deploy.sh new file mode 100644 index 00000000..c6f6414f --- /dev/null +++ b/deployment/check/data-model-tf/deploy.sh @@ -0,0 +1,35 @@ +#! /usr/bin/env bash +set -xeuo pipefail +declare username host system config_nix config_tf + +# INSTANTIATE +command=(nix-instantiate --argstr system "$system" --argstr config_nix "$config_nix" --argstr config_tf "$config_tf" ./nixos.nix) +# instantiate the config in /nix/store +"${command[@]}" -A out_path + +# DEPLOY +sshOpts=( + -o BatchMode=yes + -o StrictHostKeyChecking=no + # TODO set key for production + # ${if key-file == null then "" else "-i ${key-file}"} + # NOTE the below options are for tests + -o ConnectTimeout=1 + -o ServerAliveInterval=1 +) +destination="$username@$host" +# get the realized derivation to deploy +outPath=$(nix-store --realize "$("${command[@]}" --show-trace --eval --strict --json | jq -r '.drv_path')") +# deploy the config by nix-copy-closure +NIX_SSHOPTS="${sshOpts[*]}" nix-copy-closure --to "$destination" "$outPath" --gzip --use-substitutes +# switch the remote host to the config +# NOTE checks here are for tests - in production time-outs could be a real thing, rather than indicator of success! +# shellcheck disable=SC2029 +output=$(ssh "${sshOpts[@]}" "$destination" "nix-env --profile /nix/var/nix/profiles/system --set $outPath; nohup $outPath/bin/switch-to-configuration switch &" 2>&1) || echo "status code: $?" +echo "output: $output" +if [[ $output != *"Timeout, server $host not responding"* ]]; then + echo "non-timeout error: $output" + exit 1 +else + exit 0 +fi diff --git a/deployment/check/data-model-tf/main.tf b/deployment/check/data-model-tf/main.tf new file mode 100644 index 00000000..a10dc19f --- /dev/null +++ b/deployment/check/data-model-tf/main.tf @@ -0,0 +1,44 @@ +# hash of our code directory, used to trigger re-deploy +# FIXME calculate separately to reduce false positives +data "external" "hash" { + program = ["sh", "-c", "echo \"{\\\"hash\\\":\\\"$(nix-hash ../../..)\\\"}\""] +} + +# TF resource to build and deploy NixOS instances. +resource "terraform_data" "nixos" { + + # trigger rebuild/deploy if (FIXME?) any potentially used config/code changed, + # preventing these (20+s, build being bottleneck) when nothing changed. + # terraform-nixos separates these to only deploy if instantiate changed, + # yet building even then - which may be not as bad using deploy on remote. + # having build/deploy one resource reflects wanting to prevent no-op rebuilds + # over preventing (with less false positives) no-op deployments, + # as i could not find a way to do prevent no-op rebuilds without merging them: + # - generic resources cannot have outputs, while we want info from the instantiation (unless built on host?). + # - `data` always runs, which is slow for deploy and especially build. + triggers_replace = [ + data.external.hash.result, + var.host, + var.config_nix, + var.config_tf, + ] + + provisioner "local-exec" { + # directory to run the script from. we use the TF project root dir, + # here as a path relative from where TF is run from, + # matching calling modules' expectations on config_nix locations. + # note that absolute paths can cause false positives in triggers, + # so are generally discouraged in TF. + working_dir = path.root + environment = { + system = var.system + username = var.username + host = var.host + config_nix = var.config_nix + config_tf = replace(jsonencode(var.config_tf), "\"", "\\\"") + } + # TODO: refactor back to command="ignoreme" interpreter=concat([]) to protect sensitive data from error logs? + # TODO: build on target? + command = "sh deploy.sh" + } +} diff --git a/deployment/check/data-model-tf/nixos.nix b/deployment/check/data-model-tf/nixos.nix new file mode 100644 index 00000000..e5c2d3ed --- /dev/null +++ b/deployment/check/data-model-tf/nixos.nix @@ -0,0 +1,13 @@ +{ + system, + config_nix, + config_tf, +}: +import ../../nixos.nix { + inherit system; + configuration = + (import ../common/data-model.nix { + inherit system; + config = config_nix // builtins.fromJSON config_tf; + })."tf-deployment".tf-host.nixos-configuration; +} diff --git a/deployment/check/data-model-tf/nixosTest.nix b/deployment/check/data-model-tf/nixosTest.nix new file mode 100644 index 00000000..11ec23e2 --- /dev/null +++ b/deployment/check/data-model-tf/nixosTest.nix @@ -0,0 +1,91 @@ +{ + lib, + config, + pkgs, + inputs, + ... +}: +let + inherit (import ./constants.nix) pathToRoot pathFromRoot; + inherit (pkgs) system; + # escapedJson = v: lib.replaceStrings [ "\"" ] [ "\\\\\"" ] (lib.strings.toJSON v); + deployment-config = { + inherit pathToRoot pathFromRoot; + inherit (config) enableAcme; + acmeNodeIP = if config.enableAcme then config.nodes.acme.networking.primaryIPAddress else null; + nodeName = "target"; + }; + inherit + ((import ../common/data-model.nix { + inherit system inputs; + config = deployment-config; + })."tf-deployment".tf-host.ssh + ) + host + username + # key-file + ; + tf-vars = { + inherit host username system; + config_nix = lib.strings.toJSON deployment-config; + # config_nix = escapedJson deployment-config; + # config_tf = ; + }; + tf-env = pkgs.callPackage ./tf-env.nix { }; +in +{ + _class = "nixosTest"; + imports = [ + ../common/data-model-options.nix + ]; + + name = "deployment-model"; + sourceFileset = lib.fileset.unions [ + ../../data-model.nix + ../../function.nix + ../common/data-model.nix + ../common/data-model-options.nix + ./constants.nix + ./main.tf + ./variables.tf + ./deploy.sh + ]; + + nodes.deployer = + { pkgs, ... }: + { + # nixpkgs.config.allowUnfree = lib.mkForce true; + + environment.systemPackages = with pkgs; [ + (pkgs.callPackage ./tf.nix { }) + jq + ]; + + # needed only when building from deployer + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + ]; + }; + }; + + extraTestScript = '' + with subtest("ssh: Check the status before deployment"): + target.fail("hello 1>&2") + + with subtest("ssh: Run the deployment"): + deployer.succeed(""" + set -xeuo pipefail + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: ''export TF_VAR_${k}='${v}';'') tf-vars)} + export TF_LOG=info + + cd "${tf-env}/deployment/check/data-model-tf" + # parallelism=1: limit OOM risk + tofu apply --auto-approve -lock=false -parallelism=1 + """) + target.wait_for_unit("multi-user.target") + target.succeed("su - operator -c hello 1>&2") + ''; +} diff --git a/deployment/check/data-model-tf/setup.nix b/deployment/check/data-model-tf/setup.nix new file mode 100644 index 00000000..6a9cefc0 --- /dev/null +++ b/deployment/check/data-model-tf/setup.nix @@ -0,0 +1,19 @@ +{ + pkgs, + lib, + sources, +}: +pkgs.writeScriptBin "setup" '' + # calculated pins + echo '${lib.strings.toJSON sources}' > ./.npins.json + # generate TF lock for nix's TF providers + for category in deployment/check/data-model-tf; do + pushd "$category" + rm -rf .terraform/ + rm -f .terraform.lock.hcl + # suppress warning on architecture-specific generated lock file: + # `Warning: Incomplete lock file information for providers`. + tofu init -input=false 1>/dev/null + popd + done +'' diff --git a/deployment/check/data-model-tf/tf-env.nix b/deployment/check/data-model-tf/tf-env.nix new file mode 100644 index 00000000..cb22201d --- /dev/null +++ b/deployment/check/data-model-tf/tf-env.nix @@ -0,0 +1,31 @@ +{ + lib, + pkgs, + sources ? import ../../../npins, +}: +pkgs.stdenv.mkDerivation { + name = "tf-repo"; + src = + with lib.fileset; + toSource { + root = ../../../.; + # don't copy ignored files + fileset = intersection (gitTracked ../../../.) ../../../.; + }; + buildInputs = [ + (pkgs.callPackage ./tf.nix { }) + (pkgs.callPackage ./setup.nix { inherit sources; }) + ]; + buildPhase = '' + runHook preBuild + pushd deployment/check/data-model-tf + setup + popd + runHook postBuild + ''; + installPhase = '' + runHook preInstall + cp -r . $out + runHook postInstall + ''; +} diff --git a/deployment/check/data-model-tf/tf.nix b/deployment/check/data-model-tf/tf.nix new file mode 100644 index 00000000..8551cb82 --- /dev/null +++ b/deployment/check/data-model-tf/tf.nix @@ -0,0 +1,11 @@ +# FIXME: use overlays so this gets imported just once? +{ + pkgs, + ... +}: +let + tf = pkgs.opentofu; +in +tf.withPlugins (p: [ + p.external +]) diff --git a/deployment/check/data-model-tf/variables.tf b/deployment/check/data-model-tf/variables.tf new file mode 100644 index 00000000..c1fd0bf9 --- /dev/null +++ b/deployment/check/data-model-tf/variables.tf @@ -0,0 +1,23 @@ +variable "system" { + type = string + default = "x86_64-linux" +} + +variable "username" { + type = string + default = "root" +} + +variable "host" { + type = string +} + +variable "config_nix" { + type = string + default = "{}" +} + +variable "config_tf" { + type = map(any) + default = {} +} diff --git a/deployment/check/panel/constants.nix b/deployment/check/panel/constants.nix index ce3b5e4d..a48d9076 100644 --- a/deployment/check/panel/constants.nix +++ b/deployment/check/panel/constants.nix @@ -8,4 +8,5 @@ pathToRoot = ../../..; pathFromRoot = ./.; enableAcme = true; + useFlake = true; } diff --git a/deployment/check/panel/default.nix b/deployment/check/panel/default.nix index 03c1379f..64448280 100644 --- a/deployment/check/panel/default.nix +++ b/deployment/check/panel/default.nix @@ -15,5 +15,6 @@ runNixOSTest { pathToRoot pathFromRoot enableAcme + useFlake ; } diff --git a/deployment/check/panel/nixosTest.nix b/deployment/check/panel/nixosTest.nix index ffcb8e53..fddad457 100644 --- a/deployment/check/panel/nixosTest.nix +++ b/deployment/check/panel/nixosTest.nix @@ -128,6 +128,7 @@ in sourceFileset = lib.fileset.unions [ ./constants.nix ./deployment.nix + (config.pathToCwd + "/flake-under-test.nix") # REVIEW: I would like to be able to grab all of `/deployment` minus # `/deployment/check`, but I can't because there is a bunch of other files diff --git a/deployment/data-model-test.nix b/deployment/data-model-test.nix index ac35df39..aad95026 100644 --- a/deployment/data-model-test.nix +++ b/deployment/data-model-test.nix @@ -1,29 +1,94 @@ let inherit (import ../default.nix { }) pkgs inputs; inherit (pkgs) lib; - inherit (lib) mkOption; + inherit (lib) mkOption types; eval = module: (lib.evalModules { specialArgs = { - inherit inputs; + inherit pkgs inputs; }; modules = [ module ./data-model.nix ]; }).config; + inherit (inputs.nixops4.lib) mkDeployment; in { _class = "nix-unit"; test-eval = { + /** + This tests a very simple arrangement that features all ingredients of the Fediversity business logic: + application, resource, environment, deployment; and wires it all up in one end-to-end exercise. + - The dummy resource is a login shell made available for some user. + - The dummy application is `hello` that requires a shell to be deployed. + - The dummy environment is a single NixOS VM that hosts one login shell, for the operator. + - The dummy configuration enables the `hello` application. + This will produce a NixOps4 deployment for a NixOS VM with a login shell for the operator and `hello` available. + */ expr = let fediversity = eval ( { config, ... }: { config = { + resources.login-shell = { + description = "The operator needs to be able to log into the shell"; + request = + { ... }: + { + _class = "fediversity-resource-request"; + options = { + wheel = mkOption { + description = "Whether the login user needs root permissions"; + type = types.bool; + default = false; + }; + packages = mkOption { + description = "Packages that need to be available in the user environment"; + type = with types; attrsOf package; + }; + }; + }; + policy = + { config, ... }: + { + _class = "fediversity-resource-policy"; + options = { + username = mkOption { + description = "Username for the operator"; + type = types.str; # TODO: use the proper constraints from NixOS + }; + wheel = mkOption { + description = "Whether to allow login with root permissions"; + type = types.bool; + default = false; + }; + }; + config = { + resource-type = types.raw; # TODO: splice out the user type from NixOS + apply = + requests: + let + # Filter out requests that need wheel if policy doesn't allow it + validRequests = lib.filterAttrs ( + _name: req: !req.login-shell.wheel || config.wheel + ) requests.resources; + in + lib.optionalAttrs (validRequests != { }) { + ${config.username} = { + isNormalUser = true; + packages = + with lib; + attrValues (concatMapAttrs (_name: request: request.login-shell.packages) validRequests); + extraGroups = lib.optional config.wheel "wheel"; + }; + }; + }; + }; + }; applications.hello = { ... }: { @@ -31,19 +96,50 @@ in module = { ... }: { - options = { - enable = lib.mkEnableOption "Hello in the shell"; + options.enable = lib.mkEnableOption "Hello in the shell"; + }; + implementation = cfg: { + input = cfg; + output.resources = lib.optionalAttrs cfg.enable { + hello.login-shell.packages.hello = pkgs.hello; + }; + }; + }; + environments.single-nixos-vm = + { config, ... }: + { + resources."operator-environment".login-shell.username = "operator"; + implementation = requests: { + input = requests; + output.nixops4 = + { providers, ... }: + { + providers = { + inherit (inputs.nixops4.modules.nixops4Provider) local; + }; + resources.the-machine = { + type = providers.local.exec; + imports = [ + inputs.nixops4-nixos.modules.nixops4Resource.nixos + ]; + nixos.module = + { ... }: + { + users.users = config.resources."operator-environment".login-shell.apply { + resources = lib.filterAttrs (_name: value: value ? login-shell) ( + lib.concatMapAttrs ( + k': req: lib.mapAttrs' (k: lib.nameValuePair "${k'}.${k}") req.resources + ) requests + ); + }; + }; + }; }; - }; - implementation = - cfg: - lib.optionalAttrs cfg.enable { - dummy.login-shell.packages.hello = pkgs.hello; - }; + }; }; }; options = { - example-configuration = mkOption { + "example-configuration" = mkOption { type = config.configuration; readOnly = true; default = { @@ -51,20 +147,66 @@ in applications.hello.enable = true; }; }; + "example-deployment" = mkOption { + type = config.environments.single-nixos-vm.resource-mapping.output-type; + readOnly = true; + default = config.environments.single-nixos-vm.deployment config."example-configuration"; + }; }; } ); + resources = + fediversity.applications.hello.resources + fediversity."example-configuration".applications.hello; + hello-shell = resources.resources.hello.login-shell; + environment = fediversity.environments.single-nixos-vm.resources."operator-environment".login-shell; + result = mkDeployment { + modules = [ + (fediversity.environments.single-nixos-vm.deployment fediversity."example-configuration") + ]; + }; + in { - inherit (fediversity) - example-configuration - ; + number-of-resources = with lib; length (attrNames fediversity.resources); + inherit (fediversity) example-configuration; + hello-package-exists = hello-shell.packages ? hello; + wheel-required = hello-shell.wheel; + wheel-allowed = environment.wheel; + operator-shell = + let + operator = (environment.apply resources).operator; + in + { + inherit (operator) isNormalUser; + packages = map (p: "${p.pname}") operator.packages; + extraGroups = operator.extraGroups; + }; + deployment = { + inherit (result) _type; + deploymentFunction = lib.isFunction result.deploymentFunction; + getProviders = lib.isFunction result.getProviders; + }; }; expected = { + number-of-resources = 1; example-configuration = { enable = true; applications.hello.enable = true; }; + hello-package-exists = true; + wheel-required = false; + wheel-allowed = false; + operator-shell = { + isNormalUser = true; + packages = [ "hello" ]; + extraGroups = [ ]; + }; + deployment = { + _type = "nixops4Deployment"; + deploymentFunction = true; + getProviders = true; + }; }; }; } diff --git a/deployment/data-model.nix b/deployment/data-model.nix index 8f584af4..914a5e0f 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -1,33 +1,140 @@ { lib, config, + inputs, + pkgs, ... }: let inherit (lib) mkOption types; inherit (lib.types) - attrsOf attrTag + attrsOf deferredModuleWith - submodule - optionType functionTo + nullOr + optionType + raw + str + submodule ; functionType = import ./function.nix; - application-resources = { + application-resources = submodule { options.resources = mkOption { # TODO: maybe transpose, and group the resources by type instead type = attrsOf ( - attrTag (lib.mapAttrs (_name: resource: mkOption { type = resource.request; }) config.resources) + attrTag ( + lib.mapAttrs (_name: resource: mkOption { type = submodule resource.request; }) config.resources + ) ); }; }; + nixops4Deployment = types.deferredModuleWith { + staticModules = [ + inputs.nixops4.modules.nixops4Deployment.default + + { + _class = "nixops4Deployment"; + _module.args = { + resourceProviderSystem = pkgs.system; + resources = { }; + }; + } + ]; + }; + nixos-configuration = mkOption { + description = "A NixOS configuration."; + type = raw; + }; + host-ssh = mkOption { + description = "SSH connection info to connect to a single host."; + type = submodule { + options = { + host = mkOption { + description = "the host to access by SSH"; + type = str; + }; + username = mkOption { + description = "the SSH user to use"; + type = nullOr str; + default = null; + }; + key-file = mkOption { + description = "path to the user's SSH private key"; + type = nullOr str; + example = "/root/.ssh/id_ed25519"; + }; + }; + }; + }; + deployment-type = attrTag { + ssh-host = mkOption { + description = "A deployment by SSH to update a single existing NixOS host."; + type = submodule { + options = { + inherit nixos-configuration; + ssh = host-ssh; + }; + }; + }; + nixops4 = mkOption { + description = "A NixOps4 NixOS deployment. For an example, see https://github.com/nixops4/nixops4-nixos/blob/main/example/deployment.nix."; + type = nixops4Deployment; + }; + tf-host = mkOption { + description = "A Terraform deployment by SSH to update a single existing NixOS host."; + type = submodule { + options = { + inherit nixos-configuration; + ssh = host-ssh; + }; + }; + }; + }; in { - _class = "nixops4Deployment"; - options = { + resources = mkOption { + description = "Collection of deployment resources that can be required by applications and policed by hosting providers"; + type = attrsOf ( + submodule ( + { ... }: + { + _class = "fediversity-resource"; + options = { + description = mkOption { + description = "Description of the resource to help application module authors and hosting providers to work with it"; + type = types.str; + }; + request = mkOption { + description = "Options for declaring resource requirements by an application, a description of how the resource is consumed or accessed"; + type = deferredModuleWith { staticModules = [ { _class = "fediversity-resource-request"; } ]; }; + }; + policy = mkOption { + description = "Options for configuring the resource policy for the hosting provider, a description of how the resource is made available"; + type = deferredModuleWith { + staticModules = [ + (policy: { + _class = "fediversity-resource-policy"; + options.resource-type = mkOption { + description = "The type of resource this policy configures"; + type = types.optionType; + }; + # TODO(@fricklerhandwerk): we may want to make the function type explicit here: `application-resources -> resource-type` + options.apply = mkOption { + description = "Apply the policy to a request"; + type = functionTo policy.config.resource-type; + }; + }) + ]; + }; + }; + }; + } + ) + ); + }; applications = mkOption { description = "Collection of Fediversity applications"; type = attrsOf ( @@ -52,12 +159,13 @@ in readOnly = true; default = input: (application.config.implementation input).output; }; + # TODO(@fricklerhandwerk): this needs a better name, it's just the type config-mapping = mkOption { description = "Function type for the mapping from application configuration to required resources"; type = submodule functionType; readOnly = true; default = { - input-type = application.config.module; + input-type = submodule application.config.module; output-type = application-resources; }; }; @@ -65,6 +173,60 @@ in }) ); }; + environments = mkOption { + description = "Run-time environments for Fediversity applications to be deployed to"; + type = attrsOf ( + submodule (environment: { + _class = "fediversity-environment"; + options = { + resources = mkOption { + description = '' + Resources made available by the hosting provider, and their policies. + + Setting this is optional, but provides a place to declare that information for programmatic use in the resource mapping. + ''; + # TODO: maybe transpose, and group the resources by type instead + type = attrsOf ( + attrTag ( + lib.mapAttrs (_name: resource: mkOption { type = submodule resource.policy; }) config.resources + ) + ); + }; + implementation = mkOption { + description = "Mapping of resources required by applications to available resources; the result can be deployed"; + type = environment.config.resource-mapping.function-type; + }; + resource-mapping = mkOption { + description = "Function type for the mapping from resources to a deployment"; + type = submodule functionType; + readOnly = true; + default = { + input-type = attrsOf application-resources; + output-type = deployment-type; + }; + }; + # TODO(@fricklerhandwerk): maybe this should be a separate thing such as `fediversity-setup`, + # which makes explicit which applications and environments are available. + # then the deployments can simply be the result of the function application baked into this module. + deployment = mkOption { + description = "Generate a deployment from a configuration, by applying an environment's resource policies to the applications' resource mappings"; + type = functionTo (environment.config.resource-mapping.output-type); + readOnly = true; + default = + cfg: + # TODO: check cfg.enable.true + let + required-resources = lib.mapAttrs ( + name: application-settings: config.applications.${name}.resources application-settings + ) cfg.applications; + in + (environment.config.implementation required-resources).output; + + }; + }; + }) + ); + }; configuration = mkOption { description = "Configuration type declaring options to be set by operators"; type = optionType; diff --git a/deployment/flake-part.nix b/deployment/flake-part.nix index 952fc694..ece89360 100644 --- a/deployment/flake-part.nix +++ b/deployment/flake-part.nix @@ -21,6 +21,21 @@ inherit (pkgs.testers) runNixOSTest; inherit inputs sources; }; + + deployment-model-ssh = import ./check/data-model-ssh { + inherit (pkgs.testers) runNixOSTest; + inherit inputs sources; + }; + + deployment-model-nixops4 = import ./check/data-model-nixops4 { + inherit (pkgs.testers) runNixOSTest; + inherit inputs sources; + }; + + deployment-model-tf = import ./check/data-model-tf { + inherit (pkgs.testers) runNixOSTest; + inherit inputs sources; + }; }; }; } diff --git a/deployment/function.nix b/deployment/function.nix index d1a047f0..f0210a34 100644 --- a/deployment/function.nix +++ b/deployment/function.nix @@ -5,7 +5,6 @@ let inherit (lib) mkOption types; inherit (types) - deferredModule submodule functionTo optionType @@ -14,10 +13,10 @@ in { options = { input-type = mkOption { - type = deferredModule; + type = optionType; }; output-type = mkOption { - type = deferredModule; + type = optionType; }; function-type = mkOption { type = optionType; @@ -25,10 +24,10 @@ in default = functionTo (submodule { options = { input = mkOption { - type = submodule config.input-type; + type = config.input-type; }; output = mkOption { - type = submodule config.output-type; + type = config.output-type; }; }; }); diff --git a/deployment/nixos.nix b/deployment/nixos.nix new file mode 100644 index 00000000..c5228106 --- /dev/null +++ b/deployment/nixos.nix @@ -0,0 +1,23 @@ +{ + configuration, + system, + sources ? import ../npins, +}: +let + eval = import "${sources.nixpkgs}/nixos/lib/eval-config.nix" { + inherit system; + specialArgs = { + inherit sources; + }; + modules = [ configuration ]; + }; + os = { + inherit (eval) pkgs config options; + system = eval.config.system.build.toplevel; + inherit (eval.config.system.build) vm vmWithBootLoader; + }; +in +{ + drv_path = os.config.system.build.toplevel.drvPath; + out_path = os.config.system.build.toplevel; +} diff --git a/infra/common/nixos/users.nix b/infra/common/nixos/users.nix index 5b7e7579..ddaf9691 100644 --- a/infra/common/nixos/users.nix +++ b/infra/common/nixos/users.nix @@ -6,7 +6,7 @@ _class = "nixos"; users.users = { - root.openssh.authorizedKeys.keys = config.user.users.procolix.openssh.authorizedKeys.keys; + root.openssh.authorizedKeys.keys = config.users.users.procolix.openssh.authorizedKeys.keys; procolix = { isNormalUser = true; diff --git a/infra/common/options.nix b/infra/common/options.nix index 413f9fb9..0bf629b5 100644 --- a/infra/common/options.nix +++ b/infra/common/options.nix @@ -20,16 +20,13 @@ in ''; }; - proxmox = mkOption { - type = types.nullOr ( - types.enum [ - "procolix" - "fediversity" - ] - ); + isFediversityVm = mkOption { + type = types.bool; description = '' - The Proxmox instance. This is used for provisioning only and should be - set to `null` if the machine is not a VM. + Whether the machine is a Fediversity VM or not. This is used to + determine whether the machine should be provisioned via Proxmox or not. + Machines that are _not_ Fediversity VM could be physical machines, or + VMs that live outside Fediversity, eg. on Procolix's Proxmox. ''; }; diff --git a/infra/common/proxmox-qemu-vm.nix b/infra/common/proxmox-qemu-vm.nix index 9176d0eb..6b4970b3 100644 --- a/infra/common/proxmox-qemu-vm.nix +++ b/infra/common/proxmox-qemu-vm.nix @@ -1,10 +1,14 @@ -{ sources, ... }: +{ ... }: + { _class = "nixos"; - imports = [ - "${sources.nixpkgs}/nixos/modules/profiles/qemu-guest.nix" - ]; + ## FIXME: It would be nice, but the following leads to infinite recursion + ## in the way we currently plug `sources` in. + ## + # imports = [ + # "${sources.nixpkgs}/nixos/modules/profiles/qemu-guest.nix" + # ]; boot = { initrd = { diff --git a/infra/common/resource.nix b/infra/common/resource.nix index 26b57c29..55aa64d4 100644 --- a/infra/common/resource.nix +++ b/infra/common/resource.nix @@ -2,7 +2,6 @@ inputs, lib, config, - sources, keys, secrets, ... @@ -33,10 +32,9 @@ in ## should go into the `./nixos` subdirectory. nixos.module = { imports = [ - "${sources.agenix}/modules/age.nix" - "${sources.disko}/module.nix" ./options.nix ./nixos + ./proxmox-qemu-vm.nix ]; ## Inject the shared options from the resource's `config` into the NixOS diff --git a/infra/flake-part.nix b/infra/flake-part.nix index e970d190..34bc6e50 100644 --- a/infra/flake-part.nix +++ b/infra/flake-part.nix @@ -14,88 +14,55 @@ let mkOption evalModules filterAttrs + mapAttrs' + deepSeq ; inherit (lib.attrsets) genAttrs; - ## Given a machine's name and whether it is a test VM, make a resource module, - ## except for its missing provider. (Depending on the use of that resource, we - ## will provide a different one.) - makeResourceModule = - { vmName, isTestVm }: - { - # TODO(@fricklerhandwerk): this is terrible but IMO we should just ditch flake-parts and have our own data model for how the project is organised internally - _module.args = { - inherit - inputs - keys - secrets - ; - }; - - nixos.module.imports = [ - ./common/proxmox-qemu-vm.nix - ]; - - nixos.specialArgs = { - inherit sources; - }; - - imports = - [ - ./common/resource.nix - ] - ++ ( - if isTestVm then - [ - ../machines/operator/${vmName} - { - nixos.module.users.users.root.openssh.authorizedKeys.keys = [ - # allow our panel vm access to the test machines - keys.panel - ]; - } - ] - else - [ - ../machines/dev/${vmName} - ] - ); - fediversityVm.name = vmName; + commonResourceModule = { + # TODO(@fricklerhandwerk): this is terrible but IMO we should just ditch + # flake-parts and have our own data model for how the project is organised + # internally + _module.args = { + inherit + inputs + keys + secrets + sources + ; }; + ## FIXME: It would be preferrable to have those `sources`-related imports in + ## the modules that use them. However, doing so triggers infinite recursions + ## because of the way we propagate `sources`. `sources` must be propagated by + ## means of `specialArgs`, but this requires a bigger change. + nixos.module.imports = [ + "${sources.nixpkgs}/nixos/modules/profiles/qemu-guest.nix" + "${sources.agenix}/modules/age.nix" + "${sources.disko}/module.nix" + "${sources.home-manager}/nixos" + ]; + + imports = [ + ./common/resource.nix + ]; + }; + ## Given a list of machine names, make a deployment with those machines' ## configurations as resources. makeDeployment = vmNames: { providers, ... }: { - # XXX: this type merge is for adding `specialArgs` to resource modules - options.resources = mkOption { - type = - with lib.types; - lazyAttrsOf (submoduleWith { - class = "nixops4Resource"; - modules = [ ]; - # TODO(@fricklerhandwerk): we may want to pass through all of `specialArgs` - # once we're sure it's sane. leaving it here for better control during refactoring. - specialArgs = { - inherit sources; - }; - }); - }; - config = { - providers.local = inputs.nixops4.modules.nixops4Provider.local; - resources = genAttrs vmNames (vmName: { - type = providers.local.exec; - imports = [ - inputs.nixops4-nixos.modules.nixops4Resource.nixos - (makeResourceModule { - inherit vmName; - isTestVm = false; - }) - ]; - }); - }; + providers.local = inputs.nixops4.modules.nixops4Provider.local; + resources = genAttrs vmNames (vmName: { + type = providers.local.exec; + imports = [ + inputs.nixops4-nixos.modules.nixops4Resource.nixos + commonResourceModule + ../machines/dev/${vmName} + ]; + }); }; makeDeployment' = vmName: makeDeployment [ vmName ]; @@ -110,21 +77,29 @@ let fediversity = import ../services/fediversity; } { - garageConfigurationResource = makeResourceModule { - vmName = "test01"; - isTestVm = true; + garageConfigurationResource = { + imports = [ + commonResourceModule + ../machines/operator/test01 + ]; }; - mastodonConfigurationResource = makeResourceModule { - vmName = "test06"; # somehow `test02` has a problem - use test06 instead - isTestVm = true; + mastodonConfigurationResource = { + imports = [ + commonResourceModule + ../machines/operator/test06 # somehow `test02` has a problem - use test06 instead + ]; }; - peertubeConfigurationResource = makeResourceModule { - vmName = "test05"; - isTestVm = true; + peertubeConfigurationResource = { + imports = [ + commonResourceModule + ../machines/operator/test05 + ]; }; - pixelfedConfigurationResource = makeResourceModule { - vmName = "test04"; - isTestVm = true; + pixelfedConfigurationResource = { + imports = [ + commonResourceModule + ../machines/operator/test04 + ]; }; }; @@ -137,54 +112,63 @@ let ## this is only needed to expose NixOS configurations for provisioning ## purposes, and eventually all of this should be handled by NixOps4. options = { - nixos.module = mkOption { }; # NOTE: not just `nixos` otherwise merging will go wrong + nixos.module = mkOption { type = lib.types.deferredModule; }; # NOTE: not just `nixos` otherwise merging will go wrong nixpkgs = mkOption { }; ssh = mkOption { }; }; }; makeResourceConfig = - vm: + { vmName, isTestVm }: (evalModules { modules = [ nixops4ResourceNixosMockOptions - (makeResourceModule vm) + commonResourceModule + (if isTestVm then ../machines/operator/${vmName} else ../machines/dev/${vmName}) ]; }).config; ## Given a VM name, make a NixOS configuration for this machine. makeConfiguration = isTestVm: vmName: - let - inherit (sources) nixpkgs; - in - import "${nixpkgs}/nixos" { - modules = [ - (makeResourceConfig { inherit vmName isTestVm; }).nixos.module - ]; + import "${sources.nixpkgs}/nixos" { + configuration = (makeResourceConfig { inherit vmName isTestVm; }).nixos.module; + system = "x86_64-linux"; }; - makeVmOptions = isTestVm: vmName: { - inherit ((makeResourceConfig { inherit vmName isTestVm; }).fediversityVm) - proxmox - vmId - description - - sockets - cores - memory - diskSize - - hostPublicKey - unsafeHostPrivateKey - ; - }; + makeVmOptions = + isTestVm: vmName: + let + config = (makeResourceConfig { inherit vmName isTestVm; }).fediversityVm; + in + if config.isFediversityVm then + { + inherit (config) + vmId + description + sockets + cores + memory + diskSize + hostPublicKey + unsafeHostPrivateKey + ; + } + else + null; listSubdirectories = path: attrNames (filterAttrs (_: type: type == "directory") (readDir path)); machines = listSubdirectories ../machines/dev; testMachines = listSubdirectories ../machines/operator; + nixosConfigurations = + genAttrs machines (makeConfiguration false) + // genAttrs testMachines (makeConfiguration true); + vmOptions = + filterAttrs (_: value: value != null) # Filter out non-Fediversity VMs + (genAttrs machines (makeVmOptions false) // genAttrs testMachines (makeVmOptions true)); + in { _class = "flake"; @@ -208,10 +192,23 @@ in ) ); }; - flake.nixosConfigurations = - genAttrs machines (makeConfiguration false) - // genAttrs testMachines (makeConfiguration true); - flake.vmOptions = - genAttrs machines (makeVmOptions false) - // genAttrs testMachines (makeVmOptions true); + flake = { inherit nixosConfigurations vmOptions; }; + + perSystem = + { pkgs, ... }: + { + checks = + mapAttrs' (name: nixosConfiguration: { + name = "nixosConfigurations-${name}"; + value = nixosConfiguration.config.system.build.toplevel; + }) nixosConfigurations + // mapAttrs' (name: vmOptions: { + name = "vmOptions-${name}"; + ## Check that VM options builds/evaluates correctly. `deepSeq e1 + ## e2` evaluates `e1` strictly in depth before returning `e2`. We + ## use this trick because checks need to be derivations, which VM + ## options are not. + value = deepSeq vmOptions pkgs.hello; + }) vmOptions; + }; } diff --git a/infra/proxmox-provision.sh b/infra/proxmox-provision.sh index 42aec63b..35ceb863 100755 --- a/infra/proxmox-provision.sh +++ b/infra/proxmox-provision.sh @@ -179,15 +179,9 @@ grab_vm_options () { --log-format raw --quiet ) - proxmox=$(echo "$options" | jq -r .proxmox) vm_id=$(echo "$options" | jq -r .vmId) description=$(echo "$options" | jq -r .description) - if [ "$proxmox" != fediversity ]; then - die "I do not know how to provision things that are not Fediversity VMs, -but I got proxmox = '%s' for VM %s." "$proxmox" "$vm_name" - fi - sockets=$(echo "$options" | jq -r .sockets) cores=$(echo "$options" | jq -r .cores) memory=$(echo "$options" | jq -r .memory) diff --git a/infra/proxmox-remove.sh b/infra/proxmox-remove.sh index a8ee2de9..e2795a01 100755 --- a/infra/proxmox-remove.sh +++ b/infra/proxmox-remove.sh @@ -167,16 +167,10 @@ grab_vm_options () { --log-format raw --quiet ) - proxmox=$(echo "$options" | jq -r .proxmox) vm_id=$(echo "$options" | jq -r .vmId) - if [ "$proxmox" != fediversity ]; then - die "I do not know how to remove things that are not Fediversity VMs, - but I got proxmox = '%s' for VM %s." "$proxmox" "$vm_name" - fi - - printf 'done grabing VM options for VM %s. Found VM %d on %s Proxmox.\n' \ - "$vm_name" "$vm_id" "$proxmox" + printf 'done grabing VM options for VM %s. Got id: %d.\n' \ + "$vm_name" "$vm_id" fi } diff --git a/machines/dev/fedi200/default.nix b/machines/dev/fedi200/default.nix index c92c8d52..36383199 100644 --- a/machines/dev/fedi200/default.nix +++ b/machines/dev/fedi200/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "fedi200"; + isFediversityVm = true; vmId = 200; - proxmox = "fediversity"; description = "Testing machine for Hans"; domain = "abundos.eu"; @@ -16,10 +17,4 @@ gateway = "2a00:51c0:13:1305::1"; }; }; - - nixos.module = { - imports = [ - ../../../infra/common/proxmox-qemu-vm.nix - ]; - }; } diff --git a/machines/dev/fedi201/default.nix b/machines/dev/fedi201/default.nix index 00717597..f9b5123d 100644 --- a/machines/dev/fedi201/default.nix +++ b/machines/dev/fedi201/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "fedi201"; + isFediversityVm = true; vmId = 201; - proxmox = "fediversity"; description = "FediPanel"; domain = "abundos.eu"; @@ -19,7 +20,6 @@ nixos.module = { imports = [ - ../../../infra/common/proxmox-qemu-vm.nix ./fedipanel.nix ]; }; diff --git a/machines/dev/fedi201/fedipanel.nix b/machines/dev/fedi201/fedipanel.nix index 96a826cf..494212de 100644 --- a/machines/dev/fedi201/fedipanel.nix +++ b/machines/dev/fedi201/fedipanel.nix @@ -1,6 +1,5 @@ { config, - sources, ... }: let @@ -11,7 +10,6 @@ in imports = [ (import ../../../panel { }).module - "${sources.home-manager}/nixos" ]; security.acme = { diff --git a/machines/dev/forgejo-ci/default.nix b/machines/dev/forgejo-ci/default.nix index 901f11c0..fc520136 100644 --- a/machines/dev/forgejo-ci/default.nix +++ b/machines/dev/forgejo-ci/default.nix @@ -20,7 +20,9 @@ in ssh.host = mkForce "forgejo-ci"; fediversityVm = { + name = "forgejo-ci"; domain = "procolix.com"; + isFediversityVm = false; ipv4 = { interface = "enp1s0f0"; diff --git a/machines/dev/vm02116/default.nix b/machines/dev/vm02116/default.nix index 77253a7c..169b2149 100644 --- a/machines/dev/vm02116/default.nix +++ b/machines/dev/vm02116/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "vm02116"; + isFediversityVm = false; vmId = 2116; - proxmox = "procolix"; description = "Forgejo"; ipv4.address = "185.206.232.34"; @@ -14,7 +15,6 @@ { lib, ... }: { imports = [ - ../../../infra/common/proxmox-qemu-vm.nix ./forgejo.nix ]; diff --git a/machines/dev/vm02187/default.nix b/machines/dev/vm02187/default.nix index ab3e5d12..c085cab3 100644 --- a/machines/dev/vm02187/default.nix +++ b/machines/dev/vm02187/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "vm02187"; + isFediversityVm = false; vmId = 2187; - proxmox = "procolix"; description = "Wiki"; ipv4.address = "185.206.232.187"; @@ -14,7 +15,6 @@ { lib, ... }: { imports = [ - ../../../infra/common/proxmox-qemu-vm.nix ./wiki.nix ]; diff --git a/machines/operator/test01/default.nix b/machines/operator/test01/default.nix index fd5dc710..d4c7e235 100644 --- a/machines/operator/test01/default.nix +++ b/machines/operator/test01/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test01"; + isFediversityVm = true; vmId = 7001; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test02/default.nix b/machines/operator/test02/default.nix index c7e8fc04..28bed0a1 100644 --- a/machines/operator/test02/default.nix +++ b/machines/operator/test02/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test02"; + isFediversityVm = true; vmId = 7002; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test03/default.nix b/machines/operator/test03/default.nix index 55b86f59..4dd77d91 100644 --- a/machines/operator/test03/default.nix +++ b/machines/operator/test03/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test03"; + isFediversityVm = true; vmId = 7003; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test04/default.nix b/machines/operator/test04/default.nix index 78f9ee09..87bb0778 100644 --- a/machines/operator/test04/default.nix +++ b/machines/operator/test04/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test04"; + isFediversityVm = true; vmId = 7004; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test05/default.nix b/machines/operator/test05/default.nix index 277c7067..44043af9 100644 --- a/machines/operator/test05/default.nix +++ b/machines/operator/test05/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test05"; + isFediversityVm = true; vmId = 7005; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test06/default.nix b/machines/operator/test06/default.nix index 42a40dc3..83f9f996 100644 --- a/machines/operator/test06/default.nix +++ b/machines/operator/test06/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test06"; + isFediversityVm = true; vmId = 7006; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test11/default.nix b/machines/operator/test11/default.nix index fe955029..1015ac76 100644 --- a/machines/operator/test11/default.nix +++ b/machines/operator/test11/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test11"; + isFediversityVm = true; vmId = 7011; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test12/default.nix b/machines/operator/test12/default.nix index cfed2f84..8f2d345f 100644 --- a/machines/operator/test12/default.nix +++ b/machines/operator/test12/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test12"; + isFediversityVm = true; vmId = 7012; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test13/default.nix b/machines/operator/test13/default.nix index 1d71b6b7..dd7abef1 100644 --- a/machines/operator/test13/default.nix +++ b/machines/operator/test13/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test13"; + isFediversityVm = true; vmId = 7013; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key; diff --git a/machines/operator/test14/default.nix b/machines/operator/test14/default.nix index 6832b2c7..5a3b96e8 100644 --- a/machines/operator/test14/default.nix +++ b/machines/operator/test14/default.nix @@ -2,8 +2,9 @@ _class = "nixops4Resource"; fediversityVm = { + name = "test14"; + isFediversityVm = true; vmId = 7014; - proxmox = "fediversity"; hostPublicKey = builtins.readFile ./ssh_host_ed25519_key.pub; unsafeHostPrivateKey = builtins.readFile ./ssh_host_ed25519_key;