diff --git a/TODO.md b/TODO.md index 69d567ee..19285149 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,2 @@ -- deploy garage machine - port tests over from nixops - host supplies function to data model allocating available resources to demand diff --git a/deployment/check/data-model-basic/nixosTest.nix b/deployment/check/data-model-basic/nixosTest.nix index 52d715af..da249ef6 100644 --- a/deployment/check/data-model-basic/nixosTest.nix +++ b/deployment/check/data-model-basic/nixosTest.nix @@ -21,7 +21,6 @@ in default = mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; configuration = config."example-configuration"; }; }; diff --git a/deployment/check/data-model-nixops4/data-model.nix b/deployment/check/data-model-nixops4/data-model.nix index 34fe4b4d..0778ed7a 100644 --- a/deployment/check/data-model-nixops4/data-model.nix +++ b/deployment/check/data-model-nixops4/data-model.nix @@ -58,7 +58,6 @@ in options.default = lib.mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; configuration = config."example-configuration"; }; }; diff --git a/deployment/check/data-model-ssh/data-model.nix b/deployment/check/data-model-ssh/data-model.nix index ac2e0ca0..64fc5ebc 100644 --- a/deployment/check/data-model-ssh/data-model.nix +++ b/deployment/check/data-model-ssh/data-model.nix @@ -57,7 +57,6 @@ in default = mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; configuration = config."example-configuration"; }; }; diff --git a/deployment/check/data-model-tf-proxmox/setups/template.nix b/deployment/check/data-model-tf-proxmox/setups/template.nix index c80a709a..5ca78d4c 100644 --- a/deployment/check/data-model-tf-proxmox/setups/template.nix +++ b/deployment/check/data-model-tf-proxmox/setups/template.nix @@ -59,11 +59,10 @@ in default = mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; # normally our template is distinct, but our test cannot download build deps due to sandboxing configuration = config."example-configuration"; - }; }; + }; deploy = mkOption { default = config.default.tf-proxmox-template.run; }; diff --git a/deployment/check/data-model-tf-proxmox/setups/vm.nix b/deployment/check/data-model-tf-proxmox/setups/vm.nix index 8539bcf0..4984457b 100644 --- a/deployment/check/data-model-tf-proxmox/setups/vm.nix +++ b/deployment/check/data-model-tf-proxmox/setups/vm.nix @@ -86,10 +86,9 @@ in default = mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; configuration = config."example-configuration"; - }; }; + }; deploy = mkOption { default = config.default.tf-proxmox-vm.run; }; diff --git a/deployment/check/data-model-tf/data-model.nix b/deployment/check/data-model-tf/data-model.nix index 1629dc86..1ba49faf 100644 --- a/deployment/check/data-model-tf/data-model.nix +++ b/deployment/check/data-model-tf/data-model.nix @@ -57,10 +57,9 @@ in default = mkOption { type = config.deployment-type; default = config.environments.default.deployment { - deployment-name = "default"; configuration = config."example-configuration"; - }; }; + }; deploy = mkOption { default = config.default.tf-host.run; }; diff --git a/deployment/data-model.nix b/deployment/data-model.nix index 5b3dd9ce..86f9e876 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -34,12 +34,32 @@ let ) ); deployment-type = attrTag (pkgs.callPackage ./run { inherit inputs sources; }); + env-output = submodule { + options = { + deployments = mkOption { + type = deployment-type; + }; + ancilliaryRequests = mkOption { + type = submodule { + options = { + garage = mkOption { + type = types.listOf types.unspecified; + }; + }; + }; + default = { }; + }; + }; + }; in { options = { deployment-type = mkOption { default = deployment-type; }; + env-output = mkOption { + default = env-output; + }; resources = mkOption { description = "Collection of deployment resources that can be required by applications and policed by hosting providers"; type = attrsOf ( @@ -158,6 +178,7 @@ in lib.mapAttrs (_name: resource: mkOption { type = submodule resource.policy; }) config.resources ) ); + default = { }; }; implementation = mkOption { description = "Mapping of resources required by applications to available resources; the result can be deployed"; @@ -170,14 +191,15 @@ in input-type = submodule { options = { deployment-name = mkOption { - type = types.str; + type = types.listOf types.str; + default = [ "default" ]; }; required-resources = mkOption { type = attrsOf application-resources; }; }; }; - output-type = deployment-type; + output-type = env-output; implementation = environment.config.implementation; }; }; @@ -188,14 +210,15 @@ in input-type = submodule { options = { deployment-name = mkOption { - type = types.str; + type = types.listOf types.str; + default = [ "default" ]; }; configuration = mkOption { type = config.configuration; }; }; }; - output-type = deployment-type; + output-type = env-output; implementation = { deployment-name, diff --git a/deployment/fediversity/resources/garage/default.nix b/deployment/fediversity/resources/garage/default.nix index 6711a05c..bc54930b 100644 --- a/deployment/fediversity/resources/garage/default.nix +++ b/deployment/fediversity/resources/garage/default.nix @@ -90,6 +90,7 @@ in { _class = "fediversity-resource-policy"; options = { + enable = lib.mkEnableOption "S3-compatible storage provider Garage"; rpc = { port = mkOption { type = types.int; @@ -135,198 +136,223 @@ in resource-type = types.submodule { options = { garageSide = mkOption { - # type = types.unspecified; - # default = { }; type = types.listOf types.unspecified; default = [ ]; }; applicationSide = mkOption { - # type = types.attrsOf types.unspecified; - # default = { }; type = types.listOf types.unspecified; default = [ ]; }; + mainConfig = mkOption { + type = types.unspecified; + default = { }; + }; }; }; - apply = - requests: - let - applicationSide = map (req: req.nixos-configuration config) requests; - garageSide = - let - # generate one using openssl (somehow) - # XXX: when importing, garage tells you importing is only meant for keys previously generated by garage. is it okay to generate them using openssl? it seems to work fine - snakeoil_key = { - id = "GK22a15201acacbd51cd43e327"; - secret = "82b2b4cbef27bf8917b350d5b10a87c92fa9c8b13a415aeeea49726cf335d74e"; + apply = requests: { + applicationSide = map (req: req.nixos-configuration config) requests; + + mainConfig = nixos: { + config = { + environment.systemPackages = [ + pkgs.minio-client + pkgs.awscli + ]; + ## REVIEW: Do we want to reverse proxy the RPC and API ports? In fact, + ## shouldn't we just get rid of RPC at all, we're not using it. + networking.firewall.allowedTCPPorts = [ + 80 + 443 + config.api.port + config.rpc.port + ]; + services.garage = { + enable = true; + package = pkgs.garage_0_9; + settings = { + replication_mode = "none"; + # TODO: use a secret file + rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625"; + # TODO: why does this have to be set? is there not a sensible default? + rpc_bind_addr = "[::]:${toString config.rpc.port}"; + rpc_public_addr = "[::1]:${toString config.rpc.port}"; + s3_api.api_bind_addr = "[::]:${toString config.api.port}"; + s3_web.bind_addr = "[::]:${toString config.web.internalPort}"; + s3_web.root_domain = ".${config.web.rootDomain}"; + index = "index.html"; + s3_api.s3_region = "garage"; + s3_api.root_domain = ".${config.api.domain}"; }; - inherit (builtins) toString; - inherit (lib) optionalString concatStringsSep mkIf; - inherit (lib.strings) escapeShellArg; - inherit (lib.attrsets) filterAttrs mapAttrs'; - concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset); - ensureAccessScriptFn = - key: bucket: - { - read, - write, - owner, - }: - '' - garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ - ${escapeShellArg bucket} --key ${escapeShellArg key} + }; + services.nginx.enable = true; + systemd.services.ensure-garage = { + after = [ "garage.service" ]; + wantedBy = [ "garage.service" ]; + serviceConfig.Type = "oneshot"; + path = [ + nixos.config.services.garage.package + pkgs.perl + pkgs.awscli + ]; + }; + + }; + }; + + garageSide = + let + # generate one using openssl (somehow) + # XXX: when importing, garage tells you importing is only meant for keys previously generated by garage. is it okay to generate them using openssl? it seems to work fine + snakeoil_key = { + id = "GK22a15201acacbd51cd43e327"; + secret = "82b2b4cbef27bf8917b350d5b10a87c92fa9c8b13a415aeeea49726cf335d74e"; + }; + inherit (lib) + optionals + optionalString + concatStringsSep + lists + strings + attrsets + ; + inherit (strings) escapeShellArg; + inherit (attrsets) filterAttrs mapAttrs'; + inherit (lists) map; + concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset); + ensureAccessScriptFn = + key: bucket: + { + read, + write, + owner, + }: + '' + garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \ + ${escapeShellArg bucket} --key ${escapeShellArg key} + ''; + ensureKeyScriptFn = + key: + { + s3AccessKeyFile, + s3SecretKeyFile, + ensureAccess, + }: + '' + ## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :` + garage key import --yes -n ${escapeShellArg key} $(cat ${escapeShellArg s3AccessKeyFile}) $(cat ${escapeShellArg s3SecretKeyFile}) || : + ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} + ''; + ensureBucketScriptFn = + cfg: bucket: + { + website, + aliases, + corsRules, + }: + let + bucketArg = escapeShellArg bucket; + corsRulesJSON = escapeShellArg ( + builtins.toJSON { + CORSRules = [ + { + AllowedHeaders = corsRules.allowedHeaders; + AllowedMethods = corsRules.allowedMethods; + AllowedOrigins = corsRules.allowedOrigins; + } + ]; + } + ); + in + '' + # garage bucket info tells us if the bucket already exists + garage bucket info ${bucketArg} || garage bucket create ${bucketArg} + # TODO: should this --deny the website if `website` is false? + ${optionalString website '' + garage bucket website --allow ${bucketArg} + ''} + ${concatStringsSep "\n" ( + map (alias: '' + garage bucket alias ${bucketArg} ${escapeShellArg alias} + '') aliases + )} + ${optionalString corsRules.enable '' + garage bucket allow --read --write --owner ${bucketArg} --key tmp + # TODO: endpoint-url should not be hard-coded + aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${config.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON} + garage bucket deny --read --write --owner ${bucketArg} --key tmp + ''} + ''; + value = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://localhost:3902"; + extraConfig = '' + ## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/ + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Disable buffering to a temporary file. + proxy_max_temp_file_size 0; + ## NOTE: This page suggests many more options for the object storage + ## proxy. We should take a look. + ## https://docs.joinmastodon.org/admin/optional/object-storage-proxy/ ''; - ensureKeyScriptFn = - key: - { - s3AccessKeyFile, - s3SecretKeyFile, - ensureAccess, - }: - '' - ## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :` - garage key import --yes -n ${escapeShellArg key} $(cat ${escapeShellArg s3AccessKeyFile}) $(cat ${escapeShellArg s3SecretKeyFile}) || : - ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess} - ''; - ensureBucketScriptFn = - bucket: - { - website, - aliases, - corsRules, - }: - let - bucketArg = escapeShellArg bucket; - corsRulesJSON = escapeShellArg ( - builtins.toJSON { - CORSRules = [ - { - AllowedHeaders = corsRules.allowedHeaders; - AllowedMethods = corsRules.allowedMethods; - AllowedOrigins = corsRules.allowedOrigins; - } - ]; - } - ); - in - '' - # garage bucket info tells us if the bucket already exists - garage bucket info ${bucketArg} || garage bucket create ${bucketArg} - # TODO: should this --deny the website if `website` is false? - ${optionalString website '' - garage bucket website --allow ${bucketArg} - ''} - ${concatStringsSep "\n" ( - map (alias: '' - garage bucket alias ${bucketArg} ${escapeShellArg alias} - '') aliases - )} - ${optionalString corsRules.enable '' - garage bucket allow --read --write --owner ${bucketArg} --key tmp - # TODO: endpoint-url should not be hard-coded - aws --region ${config.settings.s3_api.s3_region} --endpoint-url ${config.api.url} s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON} - garage bucket deny --read --write --owner ${bucketArg} --key tmp - ''} - ''; - value = { - forceSSL = true; - enableACME = true; - locations."/" = { - proxyPass = "http://localhost:3902"; - extraConfig = '' - ## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/ - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # Disable buffering to a temporary file. - proxy_max_temp_file_size 0; - ## NOTE: This page suggests many more options for the object storage - ## proxy. We should take a look. - ## https://docs.joinmastodon.org/admin/optional/object-storage-proxy/ + }; + }; + ensureBucketsScript = + cfg: + lib.concatStringsSep "\n" ( + map ({ ensureBuckets, ... }: concatMapAttrs (ensureBucketScriptFn cfg) ensureBuckets) requests + ); + ensureKeysScript = lib.concatStringsSep "\n" ( + map ({ ensureKeys, ... }: concatMapAttrs ensureKeyScriptFn ensureKeys) requests + ); + baseCfg = nixos: { + config = { + systemd.services.ensure-garage = { + after = [ "garage.service" ]; + wantedBy = [ "garage.service" ]; + serviceConfig.Type = "oneshot"; + path = [ + nixos.config.services.garage.package + pkgs.perl + pkgs.awscli + ]; + script = '' + set -xeuo pipefail + # Give Garage time to start up by waiting until somethings speaks HTTP + # behind Garage's API URL. + until ${pkgs.curl}/bin/curl -sio /dev/null ${config.api.url}; do sleep 1; done + # XXX: this is very sensitive to being a single instance + # (doing the bare minimum to get garage up and running) + # also, it's crazy that we have to parse command output like this + # TODO: talk to garage maintainer about making this nicer to work with in Nix + # before I do that though, I should figure out how setting it up across multiple machines will work + GARAGE_ID=$(garage node id 2>/dev/null | perl -ne '/(.*)@.*/ && print $1') + garage layout assign -z g1 -c 1G $GARAGE_ID + LAYOUT_VER=$(garage layout show | perl -ne '/Current cluster layout version: (\d*)/ && print $1') + garage layout apply --version $((LAYOUT_VER + 1)) + # XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys + # TODO: generate this key here rather than using a well-known key + # TODO: if the key already exists, we get an error; hacked with this `|| :` which needs to be removed + garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret} || : + export AWS_ACCESS_KEY_ID=${snakeoil_key.id}; + export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}; + ${ensureBucketsScript nixos.config.services.garage} + ${ensureKeysScript} + # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time + # garage key delete ${snakeoil_key.id} --yes ''; }; }; - ensureBucketsScript = lib.concatStringsSep "\n" (lib.lists.map ( - { ensureBuckets }: concatMapAttrs ensureBucketScriptFn ensureBuckets - )) requests; - ensureKeysScript = lib.concatStringsSep "\n" (lib.lists.map ( - { ensureKeys }: concatMapAttrs ensureKeyScriptFn ensureKeys - )) requests; - baseCfg = { - config = mkIf config.enable { - environment.systemPackages = [ - pkgs.minio-client - pkgs.awscli - ]; - ## REVIEW: Do we want to reverse proxy the RPC and API ports? In fact, - ## shouldn't we just get rid of RPC at all, we're not using it. - networking.firewall.allowedTCPPorts = [ - 80 - 443 - config.api.port - config.rpc.port - ]; - services.garage = { - enable = true; - package = pkgs.garage_0_9; - settings = { - replication_mode = "none"; - # TODO: use a secret file - rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625"; - # TODO: why does this have to be set? is there not a sensible default? - rpc_bind_addr = "[::]:${toString config.rpc.port}"; - rpc_public_addr = "[::1]:${toString config.rpc.port}"; - s3_api.api_bind_addr = "[::]:${toString config.api.port}"; - s3_web.bind_addr = "[::]:${toString config.web.internalPort}"; - s3_web.root_domain = ".${config.web.rootDomain}"; - index = "index.html"; - s3_api.s3_region = "garage"; - s3_api.root_domain = ".${config.api.domain}"; - }; - }; - services.nginx.enable = true; - systemd.services.ensure-garage = { - after = [ "garage.service" ]; - wantedBy = [ "garage.service" ]; - serviceConfig.Type = "oneshot"; - path = [ - config.package - pkgs.perl - pkgs.awscli - ]; - script = '' - set -xeuo pipefail - # Give Garage time to start up by waiting until somethings speaks HTTP - # behind Garage's API URL. - until ${pkgs.curl}/bin/curl -sio /dev/null ${config.api.url}; do sleep 1; done - # XXX: this is very sensitive to being a single instance - # (doing the bare minimum to get garage up and running) - # also, it's crazy that we have to parse command output like this - # TODO: talk to garage maintainer about making this nicer to work with in Nix - # before I do that though, I should figure out how setting it up across multiple machines will work - GARAGE_ID=$(garage node id 2>/dev/null | perl -ne '/(.*)@.*/ && print $1') - garage layout assign -z g1 -c 1G $GARAGE_ID - LAYOUT_VER=$(garage layout show | perl -ne '/Current cluster layout version: (\d*)/ && print $1') - garage layout apply --version $((LAYOUT_VER + 1)) - # XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys - # TODO: generate this key here rather than using a well-known key - # TODO: if the key already exists, we get an error; hacked with this `|| :` which needs to be removed - garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret} || : - export AWS_ACCESS_KEY_ID=${snakeoil_key.id}; - export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}; - ${ensureBucketsScript} - ${ensureKeysScript} - # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time - # garage key delete ${snakeoil_key.id} --yes - ''; - }; - }; - }; - in + }; + in + optionals config.enable ( [ baseCfg ] - ++ (lib.lists.map ( - { ensureBuckets }: + ++ (map ( + { ensureBuckets, ... }: { ## Create a proxy from .web.garage. to localhost:3902 for ## each bucket that has `website = true`. @@ -335,11 +361,9 @@ in inherit value; }) (filterAttrs (_: { website, ... }: website) ensureBuckets); } - ) requests); - in - { - inherit garageSide applicationSide; - }; + ) requests) + ); + }; }; }; }; diff --git a/deployment/fediversity/ssh-host.nix b/deployment/fediversity/ssh-host.nix deleted file mode 100644 index 8fb3c920..00000000 --- a/deployment/fediversity/ssh-host.nix +++ /dev/null @@ -1,97 +0,0 @@ -{ - system, - nodeName, - network, - sources ? import ../../npins, - conf ? { }, - ... -}@args: -let - inherit (sources) nixpkgs; - pkgs = import nixpkgs { inherit system; }; - inherit (pkgs) lib; - pathToRoot = builtins.path { - path = ../..; - name = "root"; - }; - sshOpts = [ ]; -in -(pkgs.callPackage ../utils.nix { }).evalModel ( - { config, modulesPath, ... }: - { - imports = [ - # "${modulesPath}/profiles/minimal.nix" - ./resources - ./applications - ./configurations.nix - ../../infra/common/options.nix - # ../../infra/common/proxmox-qemu-vm.nix - ]; - options = { - # inherit (config) default-configuration; - # inherit (conf) default-configuration; - # conf = lib.mkOption { - # default = conf; - # }; - default = lib.mkOption { - type = config.deployment-type; - default = config.environments.default.deployment { - deployment-name = "default"; - configuration = config."default-configuration"; - }; - }; - }; - config = lib.mkMerge [ - { - environments.default = environment: { - resources."external".garage = { }; - resources."fediversity".nixos-module = { }; - resources."fixed-host".network = network; - resources."age".secrets = { }; - implementation = - { - required-resources, - deployment-name, - ... - }: - let - garage = environment.config.resources."external".garage.process required-resources; - # TODO use garage.garageSide - in - # builtins.trace required-resources.peertube.resources.peertube-bucket - { - ssh-host = { - nixos-configuration = { - imports = - [ - ../../infra/common/nixos - "${sources.disko}/module.nix" - "${modulesPath}/profiles/qemu-guest.nix" - (environment.config.resources."fixed-host".network.process required-resources) - (environment.config.resources."age".secrets.process required-resources) - ] - ++ (environment.config.resources."fediversity".nixos-module.process required-resources) - ++ garage.applicationSide; - }; - inherit system; - ssh = { - username = "root"; - host = nodeName; - key-file = null; - inherit sshOpts; - }; - caller = "deployment/fediversity/ssh-host.nix"; - inherit args deployment-name; - root-path = pathToRoot; - }; - }; - }; - } - conf - # splice global config into apps using it - { - default-configuration.applications.pixelfed = { inherit (conf.default-configuration) initialUser; }; - } - ]; - } -) diff --git a/deployment/fediversity/ssh-hosts.nix b/deployment/fediversity/ssh-hosts.nix index c759c3ea..83109941 100644 --- a/deployment/fediversity/ssh-hosts.nix +++ b/deployment/fediversity/ssh-hosts.nix @@ -1,23 +1,40 @@ { system, host-mapping, + ancilliary, sources ? import ../../npins, conf ? { }, ... -}: +}@args: let inherit (sources) nixpkgs; pkgs = import nixpkgs { inherit system; }; inherit (pkgs) lib; inherit (lib) types; - pathToRoot = builtins.path { + inherit (pkgs.callPackage ../utils.nix { }) getSomeAttrs evalModel; + caller = "deployment/fediversity/ssh-hosts.nix"; + root-path = builtins.path { path = ../..; name = "root"; }; sshOpts = [ ]; + username = "root"; + key-file = null; + apps = lib.attrNames host-mapping; + nodes = lib.attrNames ancilliary ++ apps; + hosts = ancilliary // host-mapping; + resources = + { + "external".garage.enable = true; + "fediversity".nixos-module = { }; + "age".secrets = { }; + } + // lib.mapAttrs (_: host: { + network = (import ../../machines/operator/${host}).fediversityVm; + }) hosts; in -(pkgs.callPackage ../utils.nix { }).evalModel ( - { config, ... }: +evalModel ( + { config, modulesPath, ... }: { imports = [ ./resources @@ -27,81 +44,107 @@ in ]; options = { - operator = lib.mkOption { - default = pkgs.writeShellScriptBin "deploy-apps.sh" ( - lib.concatStringsSep "\n" ( - lib.mapAttrsToList (app: _: '' - echo 'DEPLOYING APP: ${app}' - ${config.${app}} - '') host-mapping - ) - ); - }; # get a typed reference to the app deployments to expose their `run` tags = lib.mkOption { - type = types.attrsOf config.deployment-type; - default = lib.mapAttrs ( - app: _: + type = types.attrsOf config.env-output; + default = lib.genAttrs (nodes ++ [ "all" ]) ( + app: config.environments.${app}.deployment { # these are the values used in recursion, i.e. those for ssh-host.nix configuration = config."default-configuration"; - deployment-name = "default"; + deployment-name = [ + "tags" + app + ]; } - ) host-mapping; + ); + }; + operator = lib.mkOption { + type = types.path; + default = lib.getExe config.tags.all.deployments.ssh-hosts.run; }; } - // lib.mapAttrs ( - app: _: + // lib.genAttrs nodes ( + app: lib.mkOption { type = types.path; - default = lib.getExe config.tags.${app}.ssh-host.run; + default = lib.getExe config.tags.${app}.deployments.ssh-host.run; } - ) host-mapping; + ); config = lib.mkMerge [ { - environments = lib.mapAttrs (app: host: { - resources = - { - "external".garage = { }; - "fediversity".nixos-module = { }; - "age".secrets = { }; - } - // lib.mapAttrs (_: host: { - network = (import ../../machines/operator/${host}).fediversityVm; - }) host-mapping; - implementation = - { - deployment-name, - ... - }: - { - # try and use `ssh-host` since as of writing there is no plural variant - ssh-host = { - inherit system; - ssh = { - username = "root"; - host = "${host}.abundos.eu"; - key-file = null; - inherit sshOpts; - }; - inherit deployment-name; - root-path = pathToRoot; - # recursion happens on the level of ssh-single-host, so let's go by that - caller = "deployment/fediversity/ssh-host.nix"; - args = { - inherit system; - nodeName = "${host}.abundos.eu"; - network = (import ../../machines/operator/${host}).fediversityVm; - conf = lib.recursiveUpdate conf { - default-configuration.applications = lib.mapAttrs (_app: _: { enable = false; }) host-mapping // { - ${app}.enable = true; + environments = + { + "all" = + { ... }: + { + implementation = + { ... }: + { + deployments.ssh-hosts = { + inherit + system + root-path + caller + args + ; + nodes = lib.lists.map (app: { + # the separate invocations' `nixos-configuration` doubles to expose that both here and to their and this recursion + inherit (config.tags.${app}.deployments.ssh-host) nixos-configuration ssh; + deployment-name = [ + "tags" + app + ]; + }) nodes; }; }; - }; - # omitting `nixos-configuration` as it's instead passed thru recursion's `ssh-host` }; - }; - }) host-mapping; + } + // lib.mapAttrs (app: host: environment: { + inherit resources; + implementation = + { + required-resources, + deployment-name, + ... + }: + let + relevant-resources = getSomeAttrs [ app ] required-resources; + garage = environment.config.resources."external".garage.process relevant-resources; + in + { + ancilliaryRequests.garage = garage.garageSide; + # try and use `ssh-host` since as of writing there is no plural variant + deployments.ssh-host = { + inherit + system + root-path + deployment-name + caller + args + ; + ssh = { + inherit sshOpts username key-file; + host = "${host}.abundos.eu"; + }; + nixos-configuration = { + imports = + [ + ../../infra/common/nixos + "${sources.disko}/module.nix" + "${modulesPath}/profiles/qemu-guest.nix" + (environment.config.resources.${app}.network.process relevant-resources) + (environment.config.resources."age".secrets.process relevant-resources) + ] + ++ (environment.config.resources."fediversity".nixos-module.process relevant-resources) + ++ garage.applicationSide + ++ (lib.optionals (app == "garage") ( + [ garage.mainConfig ] ++ lib.concatMap (app': config.tags.${app'}.ancilliaryRequests.garage) apps + )); + }; + }; + }; + }) hosts; } conf # splice global config into apps using it diff --git a/deployment/run/default.nix b/deployment/run/default.nix index 79f442ee..fd2e657d 100644 --- a/deployment/run/default.nix +++ b/deployment/run/default.nix @@ -17,6 +17,7 @@ let submodule ; inherit (pkgs.callPackage ../utils.nix { }) + evalOption mapKeys withPackages withEnv @@ -46,7 +47,7 @@ let system = "${system}"; configuration = (import "${root-path}/${caller}" (builtins.fromJSON "${ lib.replaceStrings [ "\"" ] [ "\\\"" ] (lib.strings.toJSON args) - }")).${deployment-name}.${deployment-type}.nixos-configuration; + }")).${lib.concatStringsSep "." deployment-name}.deployments.${deployment-type}.nixos-configuration; } '' ); @@ -63,6 +64,10 @@ let } ]; }; + system = mkOption { + description = "The architecture of the system to deploy to."; + type = types.str; + }; nixos-configuration = mkOption { description = "A NixOS configuration."; type = types.unspecified; @@ -73,14 +78,18 @@ let description = "The calling module to obtain the NixOS configuration from."; type = types.str; }; + root-path = mkOption { + description = "The path to the root of the repository."; + type = types.path; + }; 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; - default = "default"; + type = types.listOf types.str; + default = [ "default" ]; }; httpBackend = mkOption { description = "environment variables to configure the TF HTTP back-end, see "; @@ -198,7 +207,7 @@ let }; }; in -{ +rec { inherit nixos-configuration; ssh-host = mkOption { description = "A deployment by SSH to update a single existing NixOS host."; @@ -206,21 +215,15 @@ in { config, ... }: { options = { - system = mkOption { - description = "The architecture of the system to deploy to."; - type = types.str; - }; inherit caller args deployment-name nixos-configuration + root-path + system ; ssh = host-ssh; - root-path = mkOption { - description = "The path to the root of the repository."; - type = types.path; - }; run = mkOption { type = types.package; default = @@ -258,7 +261,7 @@ in }; }; in - pkgs.writers.writeBashBin "deploy-sh.sh" + pkgs.writers.writeBashBin "deploy-ssh-host.sh" (withPackages [ pkgs.jq ]) @@ -270,6 +273,71 @@ in } ); }; + ssh-hosts = mkOption { + description = "A deployment by SSH to update multiple existing NixOS hosts in order."; + type = submodule ( + { config, ... }: + { + options = { + inherit + caller + args + root-path + system + ; + nodes = mkOption { + type = types.listOf (submodule { + options = { + inherit deployment-name nixos-configuration; + ssh = host-ssh; + }; + }); + }; + run = mkOption { + type = types.package; + default = + let + inherit (config) + system + caller + args + root-path + nodes + ; + in + pkgs.writers.writeBashBin "deploy-ssh-hosts.sh" ( + lib.concatStringsSep "\n" ( + lib.lists.map ( + { + deployment-name, + ssh, + nixos-configuration, + }: + '' + set -e + printf "\nDEPLOYING ${lib.concatStringsSep "." deployment-name}\n\n" + ${lib.getExe + (evalOption ssh-host { + inherit + system + root-path + caller + args + deployment-name + ssh + nixos-configuration + ; + }).run + } + '' + ) nodes + ) + ); + }; + }; + } + ); + }; nixops4 = mkOption { description = "A NixOps4 NixOS deployment. For an example, see https://github.com/nixops4/nixops4-nixos/blob/main/example/deployment.nix."; type = nixops4Deployment; @@ -280,22 +348,16 @@ in { config, ... }: { options = { - system = mkOption { - description = "The architecture of the system to deploy to."; - type = types.str; - }; inherit caller args deployment-name httpBackend nixos-configuration + root-path + system ; ssh = host-ssh; - root-path = mkOption { - description = "The path to the root of the repository."; - type = types.path; - }; run = mkOption { type = types.package; default = @@ -354,11 +416,7 @@ in { config, ... }: { options = { - system = mkOption { - description = "The architecture of the system to deploy to."; - type = types.str; - }; - inherit httpBackend nixos-configuration; + inherit httpBackend nixos-configuration system; ssh = host-ssh; node-name = mkOption { description = "the name of the ProxmoX node to use."; @@ -439,22 +497,16 @@ in { config, ... }: { options = { - system = mkOption { - description = "The architecture of the system to deploy to."; - type = types.str; - }; inherit caller args deployment-name httpBackend nixos-configuration + root-path + system ; ssh = host-ssh; - root-path = mkOption { - description = "The path to the root of the repository."; - type = types.path; - }; node-name = mkOption { description = "the name of the ProxmoX node to use."; type = types.str; diff --git a/deployment/utils-test.nix b/deployment/utils-test.nix index 4d66d200..3b8434db 100644 --- a/deployment/utils-test.nix +++ b/deployment/utils-test.nix @@ -2,6 +2,7 @@ let inherit (import ../default.nix { }) pkgs; inherit (pkgs.callPackage ./utils.nix { }) mapKeys + getSomeAttrs evalOption toBash withPackages @@ -22,6 +23,14 @@ in }; }; + test-getSomeAttrs = { + expr = getSomeAttrs [ "a" "c" ] { + a = 0; + b = 1; + }; + expected.a = 0; + }; + test-evalOption = { expr = evalOption (mkOption { type = submodule { diff --git a/deployment/utils.nix b/deployment/utils.nix index 2cdc4d1f..0e101901 100644 --- a/deployment/utils.nix +++ b/deployment/utils.nix @@ -14,6 +14,10 @@ rec { } ); + getSomeAttrs = + names: attrs: + lib.getAttrs (lib.concatMap (name: lib.optional (lib.hasAttr name attrs) name) names) attrs; + evalModel = module: (lib.evalModules { diff --git a/flake.nix b/flake.nix index c51806a1..d46990b3 100644 --- a/flake.nix +++ b/flake.nix @@ -73,6 +73,7 @@ inherit (import ./deployment/fediversity { inherit system host-mapping; + ancilliary.garage = "test01"; conf."default-configuration" = default-configuration // { enable = true; applications = lib.mapAttrs (_app: _: { enable = true; }) host-mapping;