diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index 5015d407..629cce26 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -57,6 +57,12 @@ jobs: - 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 + ## 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/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/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/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/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..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/constants.nix b/deployment/check/data-model/constants.nix new file mode 100644 index 00000000..0c8576a7 --- /dev/null +++ b/deployment/check/data-model/constants.nix @@ -0,0 +1,7 @@ +{ + targetMachines = [ + "hello" + ]; + pathToRoot = ../../..; + pathFromRoot = ./.; +} diff --git a/deployment/check/data-model/default.nix b/deployment/check/data-model/default.nix new file mode 100644 index 00000000..11c1fb37 --- /dev/null +++ b/deployment/check/data-model/default.nix @@ -0,0 +1,16 @@ +{ + 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; +} diff --git a/deployment/check/data-model/deployment.nix b/deployment/check/data-model/deployment.nix new file mode 100644 index 00000000..69380519 --- /dev/null +++ b/deployment/check/data-model/deployment.nix @@ -0,0 +1,156 @@ +{ + 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 (lib) mkOption types; + eval = + module: + (lib.evalModules { + specialArgs = { + inherit 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.single-nixos-vm = environment: { + resources."operator-environment".login-shell.username = "operator"; + implementation = requests: { + input = requests; + output.ssh-host = { + ssh = { + username = "root"; + inherit (deployment-config) host; + key-file = null; + }; + nixos-configuration = + { ... }: + { + imports = [ + ./options.nix + ../common/sharedOptions.nix + ../common/targetNode.nix + "${nixpkgs}/nixos/modules/profiles/qemu-guest.nix" + ]; + + inherit (deployment-config) enableAcme; + acmeNodeIP = + if deployment-config.enableAcme then + deployment-config.nodes.acme.networking.primaryIPAddress + else + null; + + 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 + ); + }; + }; + }; + }; + }; + }; + options = { + "example-configuration" = mkOption { + type = config.configuration; + default = { + enable = true; + applications.hello.enable = true; + }; + }; + "example-deployment" = mkOption { + type = config.environments.single-nixos-vm.resource-mapping.output-type; + default = config.environments.single-nixos-vm.deployment config."example-configuration"; + }; + }; + } + ); +in +fediversity."example-deployment" diff --git a/deployment/check/data-model/nixosTest.nix b/deployment/check/data-model/nixosTest.nix new file mode 100644 index 00000000..11c0e3a6 --- /dev/null +++ b/deployment/check/data-model/nixosTest.nix @@ -0,0 +1,114 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (import ./constants.nix) targetMachines pathToRoot; + escapedJson = v: lib.replaceStrings [ "\"" ] [ "\\\\\"" ] (lib.strings.toJSON v); +in +{ + _class = "nixosTest"; + imports = [ + ./options.nix + ]; + + name = "deployment-model"; + sourceFileset = lib.fileset.unions [ + ../../data-model.nix + ../../function.nix + ./constants.nix + ./deployment.nix + ]; + + nodes.deployer = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + jq + ]; + system.extraDependenciesFromModule = + { pkgs, ... }: + { + environment.systemPackages = with pkgs; [ + hello + ]; + }; + }; + + extraTestScript = '' + ${lib.concatStringsSep "\n" ( + lib.lists.map ( + nodeName: + let + deployment-config = { + inherit (config) enableAcme; + acmeNodeIP = if config.enableAcme then config.nodes.acme.networking.primaryIPAddress else null; + host = nodeName; + }; + inherit + ((import ./deployment.nix { + inherit (pkgs) system; + config = deployment-config; + }).ssh-host.ssh + ) + host + username + key-file + ; + in + '' + with subtest("Check the status before deployment"): + ${nodeName}.fail("${nodeName} 1>&2") + + with subtest("Run the deployment for ${nodeName}"): + 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/data-model/deployment.nix { + inherit system; + config = builtins.fromJSON "${escapedJson deployment-config}"; + } + ).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 ${nodeName} not responding"* ]]; then + echo "non-timeout error: $output" + exit 1 + else + exit 0 + fi + """) + ${nodeName}.wait_for_unit("multi-user.target") + ${nodeName}.succeed("su - operator -c ${nodeName} 1>&2") + '' + ) targetMachines + )} + ''; +} diff --git a/deployment/check/data-model/options.nix b/deployment/check/data-model/options.nix new file mode 100644 index 00000000..8492bee3 --- /dev/null +++ b/deployment/check/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/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/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..e754bc03 100644 --- a/deployment/data-model-test.nix +++ b/deployment/data-model-test.nix @@ -1,7 +1,7 @@ let inherit (import ../default.nix { }) pkgs inputs; inherit (pkgs) lib; - inherit (lib) mkOption; + inherit (lib) mkOption types; eval = module: (lib.evalModules { @@ -13,17 +13,82 @@ let ./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..90d3b0d2 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -6,28 +6,107 @@ 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 + ) ); }; }; + 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 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 +131,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 +145,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..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; + }; }; }; } 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; +}