diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 7bc68bf5..eb1dfa2f 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -56,3 +56,9 @@ jobs: steps: - uses: actions/checkout@v4 - run: nix build .#checks.x86_64-linux.deployment-panel -L + + check-deployment-model: + runs-on: native + steps: + - uses: actions/checkout@v4 + - run: nix build .#checks.x86_64-linux.deployment-model -L diff --git a/deployment/check/data-model/common-nixosTest.nix b/deployment/check/data-model/common-nixosTest.nix new file mode 100644 index 00000000..cb52ed9f --- /dev/null +++ b/deployment/check/data-model/common-nixosTest.nix @@ -0,0 +1,200 @@ +{ + inputs, + lib, + config, + hostPkgs, + sources, + ... +}: + +let + inherit (builtins) + concatStringsSep + toJSON + ; + inherit (lib) + types + fileset + mkOption + genAttrs + attrNames + optionalString + ; + inherit (hostPkgs) + runCommandNoCC + writeText + system + ; + + forConcat = xs: f: concatStringsSep "\n" (map f xs); + + ## We will need to override some inputs by the empty flake, so we make one. + emptyFlake = runCommandNoCC "empty-flake" { } '' + mkdir $out + echo "{ outputs = { self }: {}; }" > $out/flake.nix + ''; + +in +{ + _class = "nixosTest"; + + imports = [ + ./sharedOptions.nix + ]; + + options = { + ## FIXME: I wish I could just use `testScript` but with something like + ## `mkOrder` to put this module's string before something else. + extraTestScript = mkOption { }; + + sourceFileset = mkOption { + ## REVIEW: Upstream to nixpkgs? + type = types.mkOptionType { + name = "fileset"; + description = "fileset"; + descriptionClass = "noun"; + check = (x: (builtins.tryEval (fileset.unions [ x ])).success); + merge = (_: defs: fileset.unions (map (x: x.value) defs)); + }; + description = '' + A fileset that will be copied to the deployer node in the current + working directory. This should contain all the files that are + necessary to run that particular test, such as the NixOS + modules necessary to evaluate a deployment. + ''; + }; + }; + + config = { + sourceFileset = fileset.unions [ + # NOTE: not the flake itself; it will be overridden. + ../../../mkFlake.nix + ../../../flake.lock + ../../../npins + + ./sharedOptions.nix + ./targetNode.nix + ./targetResource.nix + + (config.pathToCwd + "/flake-under-test.nix") + ]; + + acmeNodeIP = config.nodes.acme.networking.primaryIPAddress; + + nodes = + { + deployer = { + imports = [ ./deployerNode.nix ]; + _module.args = { inherit inputs sources; }; + enableAcme = config.enableAcme; + acmeNodeIP = config.nodes.acme.networking.primaryIPAddress; + }; + } + + // + + ( + if config.enableAcme then + { + acme = { + ## FIXME: This makes `nodes.acme` into a local resolver. Maybe this will + ## break things once we play with DNS? + imports = [ "${inputs.nixpkgs}/nixos/tests/common/acme/server" ]; + ## We aren't testing ACME - we just want certificates. + systemd.services.pebble.environment.PEBBLE_VA_ALWAYS_VALID = "1"; + }; + } + else + { } + ) + + // + + genAttrs config.targetMachines (_: { + imports = [ ./targetNode.nix ]; + _module.args = { inherit inputs sources; }; + enableAcme = config.enableAcme; + acmeNodeIP = if config.enableAcme then config.nodes.acme.networking.primaryIPAddress else null; + }); + + testScript = '' + ${forConcat (attrNames config.nodes) (n: '' + ${n}.start() + '')} + + ${forConcat (attrNames config.nodes) (n: '' + ${n}.wait_for_unit("multi-user.target") + '')} + + ## A subset of the repository that is necessary for this test. It will be + ## copied inside the test. The smaller this set, the faster our CI, because we + ## won't need to re-run when things change outside of it. + with subtest("Unpacking"): + deployer.succeed("cp -r --no-preserve=mode ${ + fileset.toSource { + root = ../../..; + fileset = config.sourceFileset; + } + }/* .") + + with subtest("Configure the network"): + ${forConcat config.targetMachines ( + tm: + let + targetNetworkJSON = writeText "target-network.json" ( + toJSON config.nodes.${tm}.system.build.networkConfig + ); + in + '' + deployer.copy_from_host("${targetNetworkJSON}", "${config.pathFromRoot}/${tm}-network.json") + '' + )} + + with subtest("Configure the deployer key"): + deployer.succeed("""mkdir -p ~/.ssh && ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa""") + deployer_key = deployer.succeed("cat ~/.ssh/id_rsa.pub").strip() + ${forConcat config.targetMachines (tm: '' + ${tm}.succeed(f"mkdir -p /root/.ssh && echo '{deployer_key}' >> /root/.ssh/authorized_keys") + '')} + + with subtest("Configure the target host key"): + ${forConcat config.targetMachines (tm: '' + host_key = ${tm}.succeed("ssh-keyscan ${tm} | grep -v '^#' | cut -f 2- -d ' ' | head -n 1") + 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} \ + ; + """) + + ${optionalString config.enableAcme '' + with subtest("Set up handmade DNS"): + deployer.succeed("echo '${config.nodes.acme.networking.primaryIPAddress}' > ${config.pathFromRoot}/acme_server_ip") + ''} + + ${config.extraTestScript} + ''; + }; +} diff --git a/deployment/check/data-model/constants.nix b/deployment/check/data-model/constants.nix new file mode 100644 index 00000000..3cf28d8f --- /dev/null +++ b/deployment/check/data-model/constants.nix @@ -0,0 +1,8 @@ +{ + targetMachines = [ + "hello" + "cowsay" + ]; + pathToRoot = ../../..; + pathFromRoot = ./.; +} diff --git a/deployment/check/data-model/default.nix b/deployment/check/data-model/default.nix new file mode 100644 index 00000000..6479b6be --- /dev/null +++ b/deployment/check/data-model/default.nix @@ -0,0 +1,14 @@ +{ + runNixOSTest, + inputs, + sources, +}: + +runNixOSTest { + imports = [ + ../common/nixosTest.nix + ./nixosTest.nix + ]; + _module.args = { inherit inputs sources; }; + inherit (import ./constants.nix) targetMachines pathToRoot pathFromRoot; +} diff --git a/deployment/check/data-model/deployment.nix b/deployment/check/data-model/deployment.nix new file mode 100644 index 00000000..14a35ac6 --- /dev/null +++ b/deployment/check/data-model/deployment.nix @@ -0,0 +1,36 @@ +{ + inputs, + sources, + lib, + providers, + ... +}: + +let + inherit (import ./constants.nix) targetMachines pathToRoot pathFromRoot; +in + +{ + providers = { + inherit (inputs.nixops4.modules.nixops4Provider) local; + }; + + resources = lib.genAttrs targetMachines (nodeName: { + type = providers.local.exec; + + imports = [ + inputs.nixops4-nixos.modules.nixops4Resource.nixos + ../common/targetResource.nix + ]; + + _module.args = { inherit inputs sources; }; + + inherit nodeName pathToRoot pathFromRoot; + + nixos.module = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.${nodeName} ]; + }; + }); +} diff --git a/deployment/check/data-model/flake-under-test.nix b/deployment/check/data-model/flake-under-test.nix new file mode 100644 index 00000000..b9e3fb4b --- /dev/null +++ b/deployment/check/data-model/flake-under-test.nix @@ -0,0 +1,22 @@ +{ + inputs = { + nixops4.follows = "nixops4-nixos/nixops4"; + nixops4-nixos.url = "github:nixops4/nixops4-nixos"; + }; + + outputs = + inputs: + import ./mkFlake.nix inputs ( + { inputs, sources, ... }: + { + imports = [ + inputs.nixops4.modules.flake.default + ]; + + nixops4Deployments.check-deployment-basic = { + imports = [ ./deployment/check/basic/deployment.nix ]; + _module.args = { inherit inputs sources; }; + }; + } + ); +} diff --git a/deployment/check/data-model/nixosTest.nix b/deployment/check/data-model/nixosTest.nix new file mode 100644 index 00000000..93c8ad23 --- /dev/null +++ b/deployment/check/data-model/nixosTest.nix @@ -0,0 +1,48 @@ +{ inputs, lib, ... }: + +{ + _class = "nixosTest"; + + name = "deployment-basic"; + + sourceFileset = lib.fileset.unions [ + ./constants.nix + ./deployment.nix + ]; + + nodes.deployer = + { pkgs, ... }: + { + environment.systemPackages = [ + inputs.nixops4.packages.${pkgs.system}.default + ]; + + # FIXME: sad times + system.extraDependencies = with pkgs; [ + jq + jq.inputDerivation + ]; + + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + cowsay + ]; + }; + }; + + extraTestScript = '' + with subtest("Check the status before deployment"): + hello.fail("hello 1>&2") + cowsay.fail("cowsay 1>&2") + + with subtest("Run the deployment"): + deployer.succeed("nixops4 apply check-deployment-basic --show-trace --no-interactive 1>&2") + + with subtest("Check the deployment"): + hello.succeed("hello 1>&2") + cowsay.succeed("cowsay hi 1>&2") + ''; +} diff --git a/deployment/flake-part.nix b/deployment/flake-part.nix index 952fc694..ca80f247 100644 --- a/deployment/flake-part.nix +++ b/deployment/flake-part.nix @@ -21,6 +21,11 @@ inherit (pkgs.testers) runNixOSTest; inherit inputs sources; }; + + deployment-model = import ./check/data-model { + inherit (pkgs.testers) runNixOSTest; + inherit inputs sources; + }; }; }; }