diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 079c9527..9b3495c5 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -64,6 +64,18 @@ jobs: - 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 + ## 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 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..57dc88c5 --- /dev/null +++ b/deployment/check/common/data-model-options.nix @@ -0,0 +1,25 @@ +{ + lib, + ... +}: +let + inherit (lib) mkOption types; +in +{ + options = { + host = mkOption { + type = types.str; + description = "name of the host to deploy to"; + }; + targetSystem = mkOption { + type = types.str; + description = "name of the host to deploy to"; + }; + sshOpts = mkOption { + description = "Extra SSH options (`-o`) to use."; + type = types.listOf types.str; + default = [ ]; + example = "ConnectTimeout=60"; + }; + }; +} diff --git a/deployment/check/common/data-model.nix b/deployment/check/common/data-model.nix new file mode 100644 index 00000000..27dd8149 --- /dev/null +++ b/deployment/check/common/data-model.nix @@ -0,0 +1,222 @@ +{ + config, + system, + inputs ? (import ../../../default.nix { }).inputs, # XXX can't be serialized + sources ? import ../../../npins, + ... +}@args: + +let + # having this module's location (`self`) and (serializable) `args`, we know + # enough to make it re-call itself to extract different info elsewhere later. + # we use this to make a deployment script using the desired nixos config, + # which would otherwise not be serializable, while nix also makes it hard to + # produce its derivation to pass thru without a `nix-instantiate` call, + # which in turn would need to be passed the (unserializable) nixos config. + self = "deployment/check/common/data-model.nix"; + inherit (sources) nixpkgs; + pkgs = import nixpkgs { inherit system; }; + inherit (pkgs) lib; + deployment-config = config; + inherit (deployment-config) + nodeName + pathToRoot + targetSystem + sshOpts + ; + 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: { + 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 = + { + required-resources, + deployment-name, + }: + { + ssh-host = { + nixos-configuration = mkNixosConfiguration environment required-resources; + system = targetSystem; + ssh = { + username = "root"; + host = nodeName; + key-file = null; + inherit sshOpts; + }; + module = self; + inherit args deployment-name; + root-path = pathToRoot; + }; + }; + }; + single-nixos-vm-nixops4 = environment: { + resources."operator-environment".login-shell.username = "operator"; + implementation = + { + required-resources, + ... + }: + { + 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 required-resources; + _module.args = { inherit inputs sources; }; + inherit (deployment-config) nodeName pathToRoot pathFromRoot; + }; + }; + }; + }; + }; + }; + 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 { + deployment-name = "ssh-deployment"; + configuration = config."example-configuration"; + }; + }; + "nixops4-deployment" = + let + env = config.environments."single-nixos-vm-nixops4"; + in + mkOption { + type = env.resource-mapping.output-type; + default = env.deployment { + deployment-name = "nixops4-deployment"; + configuration = 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 bc30edba..93bd3fef 100644 --- a/deployment/check/common/nixosTest.nix +++ b/deployment/check/common/nixosTest.nix @@ -76,8 +76,6 @@ in ./sharedOptions.nix ./targetNode.nix ./targetResource.nix - - (config.pathToCwd + "/flake-under-test.nix") ]; acmeNodeIP = config.nodes.acme.networking.primaryIPAddress; @@ -164,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..a7e4e41f 100644 --- a/deployment/check/common/sharedOptions.nix +++ b/deployment/check/common/sharedOptions.nix @@ -32,11 +32,11 @@ in }; pathFromRoot = mkOption { - type = types.path; + type = types.either types.path types.str; description = '' Path from the root of the repository to the working directory. ''; - apply = x: lib.path.removePrefix config.pathToRoot x; + apply = x: if lib.isString x then x else lib.path.removePrefix config.pathToRoot x; }; pathToCwd = mkOption { @@ -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..0755c42e --- /dev/null +++ b/deployment/check/data-model-ssh/constants.nix @@ -0,0 +1,12 @@ +{ + targetMachines = [ + "ssh" + ]; + # stablize path, as just the path would yield distinct paths when applied multiple times + pathToRoot = builtins.path { + path = ../../..; + name = "root"; + }; + pathFromRoot = "/deployment/check/data-model-ssh"; + 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..0e129493 --- /dev/null +++ b/deployment/check/data-model-ssh/nixosTest.nix @@ -0,0 +1,72 @@ +{ + lib, + pkgs, + ... +}: +let + inherit (import ./constants.nix) pathToRoot pathFromRoot; + inherit (pkgs) system; + deployment-config = { + inherit pathToRoot pathFromRoot; + nodeName = "ssh"; + targetSystem = system; + sshOpts = [ + "ConnectTimeout=1" + "ServerAliveInterval=1" + ]; + }; + deploy = + (import ../common/data-model.nix { + inherit system; + config = deployment-config; + # opt not to pass `inputs`, as we could only pass serializable arguments through to its self-call + })."ssh-deployment".ssh-host.run; +in +{ + _class = "nixosTest"; + imports = [ + ../common/data-model-options.nix + ]; + + name = "deployment-model"; + sourceFileset = lib.fileset.unions [ + ../../data-model.nix + ../../function.nix + ../../nixos.nix + ../../run/ssh-single-host/run.sh + ../../../npins/default.nix + ../../../npins/sources.json + ../common/data-model.nix + ../common/data-model-options.nix + ./constants.nix + ]; + + nodes.deployer = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + jq + deploy + ]; + + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + ]; + }; + }; + + extraTestScript = '' + with subtest("Check the status before deployment"): + ssh.fail("hello 1>&2") + + with subtest("Run the deployment"): + deployer.succeed(""" + ${lib.getExe deploy} + """) + ssh.wait_for_unit("multi-user.target") + ssh.succeed("su - operator -c hello 1>&2") + ''; +} 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 32de523b..ff8058d3 100644 --- a/deployment/data-model-test.nix +++ b/deployment/data-model-test.nix @@ -6,7 +6,7 @@ let module: (lib.evalModules { specialArgs = { - inherit inputs; + inherit pkgs inputs; }; modules = [ module @@ -31,7 +31,7 @@ in expr = let fediversity = eval ( - { config, options, ... }: + { config, ... }: { config = { resources.login-shell = { @@ -99,15 +99,16 @@ in options.enable = lib.mkEnableOption "Hello in the shell"; }; implementation = - cfg: - lib.optionalAttrs cfg.enable { - resources.hello.login-shell.packages.hello = pkgs.hello; + cfg: { + resources = lib.optionalAttrs cfg.enable { + hello.login-shell.packages.hello = pkgs.hello; }; + }; }; environments.single-nixos-vm = { config, ... }: { - resources.operator-environment.login-shell.username = "operator"; + resources."operator-environment".login-shell.username = "operator"; implementation = requests: { nixops4 = ( { providers, ... }: @@ -123,9 +124,13 @@ in nixos.module = { ... }: { - users.users = config.resources.shell.login-shell.apply ( - lib.filterAttrs (_name: value: value ? login-shell) requests - ); + 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 + ); + }; }; }; } @@ -134,7 +139,7 @@ in }; }; options = { - example-configuration = mkOption { + "example-configuration" = mkOption { type = config.configuration; readOnly = true; default = { @@ -142,20 +147,22 @@ in applications.hello.enable = true; }; }; - example-deployment = mkOption { - type = options.deployments.nestedType; + "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; + default = config.environments.single-nixos-vm.deployment config."example-configuration"; }; }; } ); - resources = fediversity.applications.hello.resources fediversity.example-configuration.applications.hello; + 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; + environment = fediversity.environments.single-nixos-vm.resources."operator-environment".login-shell; result = mkDeployment { modules = [ - (fediversity.environments.single-nixos-vm.deployment fediversity.example-configuration) + (fediversity.environments.single-nixos-vm.deployment fediversity."example-configuration") ]; }; diff --git a/deployment/data-model.nix b/deployment/data-model.nix index edd969a5..8a71efca 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -2,18 +2,32 @@ lib, config, inputs, + pkgs, ... }: let inherit (lib) mkOption types; inherit (lib.types) - attrsOf attrTag + attrsOf deferredModuleWith - submodule - optionType functionTo + nullOr + optionType + raw + str + submodule ; + toBash = + v: + lib.replaceStrings [ "\"" ] [ "\\\"" ] ( + if lib.isPath v || builtins.isNull v then + toString v + else if lib.isString v then + v + else + lib.strings.toJSON v + ); functionType = submodule ./function.nix; application-resources = submodule { @@ -33,13 +47,114 @@ let { _class = "nixops4Deployment"; _module.args = { - resourceProviderSystem = builtins.currentSystem; + resourceProviderSystem = pkgs.system; resources = { }; }; } ]; }; - deployment = attrTag { + 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"; + }; + sshOpts = mkOption { + description = "Extra SSH options (`-o`) to use."; + type = types.listOf str; + default = [ ]; + example = "ConnectTimeout=60"; + }; + }; + }; + }; + deployment-type = attrTag { + ssh-host = mkOption { + description = "A deployment by SSH to update a single existing NixOS host."; + type = submodule (ssh-host: { + options = { + system = mkOption { + description = "The architecture of the system to deploy to."; + type = types.str; + }; + inherit nixos-configuration; + ssh = host-ssh; + module = mkOption { + description = "The module to call to obtain the NixOS configuration from."; + type = types.str; + }; + args = mkOption { + description = "The arguments with which to call the module to obtain the NixOS configuration."; + type = types.attrs; + }; + deployment-name = mkOption { + description = "The name of the deployment for which to obtain the NixOS configuration."; + type = types.str; + }; + root-path = mkOption { + description = "The path to the root of the repository."; + type = types.path; + }; + run = mkOption { + type = types.package; + # error: The option `ssh-deployment.ssh-host.run' is read-only, but it's set multiple times. + # readOnly = true; + default = + let + inherit (ssh-host.config) + system + ssh + module + args + deployment-name + root-path + ; + inherit (ssh) + host + username + key-file + sshOpts + ; + environment = { + key_file = key-file; + deployment_name = deployment-name; + root_path = root-path; + ssh_opts = sshOpts; + inherit + system + host + username + module + args + ; + deployment_type = "ssh-host"; + }; + in + pkgs.writeShellScriptBin "deploy-ssh.sh" '' + env ${ + toString (lib.mapAttrsToList (k: v: "${k}=\"${toBash v}\"") environment) + } bash ./deployment/run/ssh-single-host/run.sh + ''; + }; + }; + }); + }; nixops4 = mkOption { description = "A NixOps4 NixOS deployment. For an example, see https://github.com/nixops4/nixops4-nixos/blob/main/example/deployment.nix."; type = nixops4Deployment; @@ -74,8 +189,7 @@ in description = "The type of resource this policy configures"; type = types.optionType; }; - # TODO(@fricklerhandwerk): we may want to make the function type explict here: `request -> resource-type` - # and then also rename this to be consistent with the application's resource mapping + # 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; @@ -166,13 +280,13 @@ in }; }; }; - output-type = deployment; + output-type = deployment-type; implementation = environment.config.implementation; }; }; config-mapping = mkOption { description = "Mapping from a configuration to a deployment"; - type = submodule functionType; + type = functionType; readOnly = true; default = { input-type = submodule { @@ -185,7 +299,7 @@ in }; }; }; - output-type = deployment; + output-type = deployment-type; implementation = { deployment-name, diff --git a/deployment/flake-part.nix b/deployment/flake-part.nix index 32e0ef02..b2c9ff89 100644 --- a/deployment/flake-part.nix +++ b/deployment/flake-part.nix @@ -26,6 +26,16 @@ 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; + }; }; }; } diff --git a/deployment/nixos.nix b/deployment/nixos.nix new file mode 100644 index 00000000..1a735222 --- /dev/null +++ b/deployment/nixos.nix @@ -0,0 +1,25 @@ +{ + configuration, + system, + sources ? import ../npins, +}: +let + eval = import "${sources.nixpkgs}/nixos/lib/eval-config.nix" { + inherit system; + specialArgs = { + inherit sources; + }; + modules = [ configuration ]; + }; + toplevel = + { + inherit (eval) pkgs config options; + system = eval.config.system.build.toplevel; + inherit (eval.config.system.build) vm vmWithBootLoader; + } + .config.system.build.toplevel; +in +{ + drv_path = toplevel.drvPath; + out_path = toplevel; +} diff --git a/deployment/run/ssh-single-host/run.sh b/deployment/run/ssh-single-host/run.sh new file mode 100755 index 00000000..058f8267 --- /dev/null +++ b/deployment/run/ssh-single-host/run.sh @@ -0,0 +1,49 @@ +#! /usr/bin/env bash +set -xeuo pipefail +declare username host system module args deployment_name deployment_type key_file root_path ssh_opts +IFS=" " read -r -a ssh_opts <<< "$( (echo "$ssh_opts" | jq -r '@sh') | tr -d \'\")" + +# DEPLOY +sshOpts=( + -o BatchMode=yes + -o StrictHostKeyChecking=no +) +for ssh_opt in "${ssh_opts[@]}"; do + sshOpts+=( + -o "$ssh_opt" + ) +done +if [[ -n "$key_file" ]]; then + sshOpts+=( + -i "$key_file" + ) +fi +destination="$username@$host" + +command=(nix-instantiate --show-trace --expr " + import $root_path/deployment/nixos.nix { + system = \"$system\"; + configuration = (import \"$root_path/$module\" (builtins.fromJSON ''$args'')).$deployment_name.$deployment_type.nixos-configuration; + } +") + +# INSTANTIATE +# instantiate the config in /nix/store +"${command[@]}" -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 "$destination" "$outPath" --gzip --use-substitutes +# switch the remote host to the config +# shellcheck disable=SC2029 +ssh "${sshOpts[@]}" "$destination" "nix-env --profile /nix/var/nix/profiles/system --set $outPath" +# shellcheck disable=SC2029 +output=$(ssh "${sshOpts[@]}" "$destination" "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