{ pkgs, lib, config, inputs, sources ? import ../npins, ... }: let inherit (lib) mkOption types; inherit (lib.types) attrTag attrsOf deferredModuleWith functionTo nullOr optionType raw str submodule ; inherit (pkgs.callPackage ./utils.nix { }) toBash; withPackages = packages: { makeWrapperArgs = [ "--prefix" "PATH" ":" "${lib.makeBinPath packages}" ]; }; writeConfig = { system, caller, root-path, deployment-type, deployment-name, args, }: # having a `caller` location and (serializable) `args`, we know # enough to call it again 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. builtins.toString ( pkgs.writers.writeText "configuration.nix" '' import ${root-path}/deployment/nixos.nix { system = "${system}"; configuration = (import "${root-path}/${caller}" (builtins.fromJSON "${ lib.replaceStrings [ "\"" ] [ "\\\"" ] (lib.strings.toJSON args) }")).${deployment-name}.${deployment-type}.nixos-configuration; } '' ); functionType = submodule ./function.nix; 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 = submodule resource.request; }) config.resources ) ); }; }; nixops4Deployment = types.deferredModuleWith { staticModules = [ inputs.nixops4.modules.nixops4Deployment.default { _class = "nixops4Deployment"; _module.args = { resourceProviderSystem = pkgs.system; resources = { }; }; } ]; }; nixos-configuration = mkOption { description = "A NixOS configuration."; type = raw; }; host-ssh = mkOption { description = "SSH connection info to connect to a single host."; type = submodule { options = { host = mkOption { description = "the host to access by SSH"; type = str; }; username = mkOption { description = "the SSH user to use"; type = nullOr str; default = null; }; key-file = mkOption { description = "path to the user's SSH private key"; type = nullOr str; example = "/root/.ssh/id_ed25519"; }; sshOpts = mkOption { description = "Extra SSH options (`-o`) to use."; type = types.listOf str; default = [ ]; example = "ConnectTimeout=60"; }; }; }; }; # FIXME allow custom deployment types # FIXME make deployments environment resources? 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; caller = mkOption { description = "The calling module 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; default = "default"; }; 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 caller args deployment-name root-path ; inherit (ssh) host username key-file sshOpts ; environment = { key_file = key-file; ssh_opts = sshOpts; inherit host username ; nixos_conf = writeConfig { inherit system caller args deployment-name root-path ; deployment-type = "ssh-host"; }; }; in pkgs.writers.writeBashBin "deploy-sh.sh" (withPackages [ pkgs.jq ]) '' 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; }; tf-host = mkOption { description = "A Terraform deployment by SSH to update a single existing NixOS host."; type = submodule (tf-host: { options = { system = mkOption { description = "The architecture of the system to deploy to."; type = types.str; }; inherit nixos-configuration; ssh = host-ssh; caller = mkOption { description = "The calling module 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; }; httpBackend = mkOption { description = "environment variables to configure the TF HTTP back-end, see "; type = types.attrsOf (types.either types.str types.int); }; run = mkOption { type = types.package; # error: The option `tf-deployment.tf-host.run' is read-only, but it's set multiple times. # readOnly = true; default = let inherit (tf-host.config) system ssh caller args deployment-name root-path httpBackend ; inherit (ssh) host username key-file sshOpts ; environment = { key_file = key-file; ssh_opts = sshOpts; inherit host username ; nixos_conf = writeConfig { inherit system caller args deployment-name root-path ; deployment-type = "tf-host"; }; }; tf-env = pkgs.callPackage ./run/tf-env.nix { inherit httpBackend; tfPackage = pkgs.callPackage ./run/tf-single-host/tf.nix { }; tfDirs = [ "deployment/run/tf-single-host" ]; }; in pkgs.writers.writeBashBin "deploy-tf.sh" (withPackages [ pkgs.jq (pkgs.callPackage ./run/tf-single-host/tf.nix { }) ]) '' env ${toString (lib.mapAttrsToList (k: v: "TF_VAR_${k}=\"${toBash v}\"") environment)} \ ${toString (lib.mapAttrsToList (k: v: "${k}=\"${toBash v}\"") httpBackend)} \ tf_env=${tf-env} bash ./deployment/run/tf-single-host/run.sh ''; }; }; }); }; tf-proxmox-template = mkOption { description = '' A Terraform deployment to upload a virtual machine template to ProxmoX VE. Proxmox credentials should be set using [environment variables] (https://registry.terraform.io/providers/bpg/proxmox/latest/docs#environment-variables-summary) with role `PVEDatastoreAdmin`. ''; type = submodule (tf-host: { options = { system = mkOption { description = "The architecture of the system to deploy to."; type = types.str; }; inherit nixos-configuration; ssh = host-ssh; node-name = mkOption { description = "the name of the ProxmoX node to use."; type = types.str; }; httpBackend = mkOption { description = "environment variables to configure the TF HTTP back-end, see "; type = types.attrsOf (types.either types.str types.int); }; imageDatastoreId = mkOption { description = "ID of the datastore of the image."; type = types.str; default = "local"; }; run = mkOption { type = types.package; # error: The option `tf-deployment.tf-host.run' is read-only, but it's set multiple times. # readOnly = true; default = let inherit (tf-host.config) system ssh httpBackend node-name imageDatastoreId ; inherit (ssh) host ; machine = import ./nixos.nix { inherit sources system; configuration = tf-host.config.nixos-configuration; }; name = "monkey"; # worse for cross-compilation, better for pre-/post-processing, needs manual `imageSize`, random failures: https://github.com/nix-community/disko/issues/550#issuecomment-2503736973 raw = "${machine.config.system.build.diskoImages}/main.raw"; environment = { inherit host ; node_name = node-name; image_datastore_id = imageDatastoreId; }; tf-env = pkgs.callPackage ./run/tf-env.nix { inherit httpBackend; tfPackage = pkgs.callPackage ./run/tf-proxmox-template/tf.nix { }; tfDirs = [ "deployment/run/tf-proxmox-template" ]; }; in lib.trace (lib.strings.toJSON environment) pkgs.writers.writeBashBin "deploy-tf-proxmox-template.sh" (withPackages [ pkgs.jq pkgs.qemu (pkgs.callPackage ./run/tf-proxmox-vm/tf.nix { }) ]) '' set -e # nixos-generate gives the burden of building revisions, while systemd-repart handles partitioning ~~at the burden of version revisions~~ # .qcow2 is around half the size of .raw, on top of supporting backups - be it apparently at the cost of performance qemu-img convert -f raw -O qcow2 -C "${raw}" /tmp/${name}.qcow2 ls -l ${raw} >&2 ls -l /tmp/${name}.qcow2 >&2 env ${toString (lib.mapAttrsToList (k: v: "TF_VAR_${k}=\"${toBash v}\"") environment)} \ ${toString (lib.mapAttrsToList (k: v: "${k}=\"${toBash v}\"") httpBackend)} \ TF_VAR_image=/tmp/${name}.qcow2 \ tf_env=${tf-env} bash ./deployment/run/tf-proxmox-template/run.sh ''; }; }; }); }; tf-proxmox-vm = mkOption { description = '' A Terraform deployment to provision and update a virtual machine on ProxmoX VE. Proxmox credentials should be set using [environment variables] (https://registry.terraform.io/providers/bpg/proxmox/latest/docs#environment-variables-summary) with roles `PVEVMAdmin PVEDatastoreAdmin PVESDNUser`. ''; type = submodule (tf-host: { options = { system = mkOption { description = "The architecture of the system to deploy to."; type = types.str; }; inherit nixos-configuration; ssh = host-ssh; caller = mkOption { description = "The calling module 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; }; node-name = mkOption { description = "the name of the ProxmoX node to use."; type = types.str; }; httpBackend = mkOption { description = "environment variables to configure the TF HTTP back-end, see "; type = types.attrsOf (types.either types.str types.int); }; bridge = mkOption { description = "The name of the network bridge (defaults to vmbr0)."; type = types.str; default = "vmbr0"; }; vlanId = mkOption { description = "The VLAN identifier."; type = types.int; default = 0; }; imageDatastoreId = mkOption { description = "ID of the datastore of the image."; type = types.str; default = "local"; }; templateId = mkOption { description = "ID of the template file from which to clone the VM."; type = types.nullOr types.str; example = "local:import/template.qcow2"; }; vmDatastoreId = mkOption { description = "ID of the datastore of the VM."; type = types.str; default = "local"; }; cdDatastoreId = mkOption { description = "ID of the datastore of the virtual CD-rom drive to use for cloud-init."; type = types.str; default = "local"; }; ipv4Gateway = mkOption { description = "Gateway for IPv4."; type = types.str; default = ""; }; ipv4Address = mkOption { description = "IPv4 address."; type = types.str; default = ""; }; ipv6Gateway = mkOption { description = "Gateway for IPv6."; type = types.str; default = ""; }; ipv6Address = mkOption { description = "IPv6 address."; type = types.str; default = ""; }; run = mkOption { type = types.package; # error: The option `tf-deployment.tf-host.run' is read-only, but it's set multiple times. # readOnly = true; default = let inherit (tf-host.config) system ssh caller args deployment-name httpBackend root-path node-name bridge vlanId imageDatastoreId templateId vmDatastoreId cdDatastoreId ipv4Gateway ipv4Address ipv6Gateway ipv6Address ; inherit (ssh) host username key-file sshOpts ; deployment-type = "tf-proxmox-vm"; nixos_conf = writeConfig { inherit system caller args deployment-name root-path deployment-type ; }; environment = { key_file = key-file; ssh_opts = sshOpts; inherit host nixos_conf bridge ; node_name = node-name; ssh_user = username; vlan_id = vlanId; image_datastore_id = imageDatastoreId; template_id = templateId; vm_datastore_id = vmDatastoreId; cd_datastore_id = cdDatastoreId; ipv4_gateway = ipv4Gateway; ipv4_address = ipv4Address; ipv6_gateway = ipv6Gateway; ipv6_address = ipv6Address; }; tf-env = pkgs.callPackage ./run/tf-env.nix { inherit httpBackend; tfPackage = pkgs.callPackage ./run/tf-proxmox-vm/tf.nix { }; tfDirs = [ "deployment/run/tf-single-host" "deployment/run/tf-proxmox-vm" ]; }; vm_name = "test14"; in lib.trace (lib.strings.toJSON environment) pkgs.writers.writeBashBin "deploy-tf-proxmox-vm.sh" (withPackages [ pkgs.jq pkgs.qemu (pkgs.callPackage ./run/tf-proxmox-vm/tf.nix { }) ]) '' set -e # TODO after install: $nix_host_keys # cp $tmpdir/${vm_name}_host_key /mnt/etc/ssh/ssh_host_ed25519_key # chmod 600 /mnt/etc/ssh/ssh_host_ed25519_key # cp $tmpdir/${vm_name}_host_key.pub /mnt/etc/ssh/ssh_host_ed25519_key.pub # chmod 644 /mnt/etc/ssh/ssh_host_ed25519_key.pub env ${ toString ( lib.mapAttrsToList (k: v: "TF_VAR_${k}=\"${toBash v}\"") ( lib.filterAttrs (_: v: v != null) environment ) ) } \ ${toString (lib.mapAttrsToList (k: v: "${k}=\"${toBash v}\"") httpBackend)} \ tf_env=${tf-env} bash ./deployment/run/tf-proxmox-vm/run.sh ''; }; }; }); }; }; in { 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` # and then also rename this to be consistent with the application's resource mapping 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 ( submodule (application: { _class = "fediversity-application"; options = { description = mkOption { description = "Description to be shown in the application overview"; type = types.str; }; module = mkOption { description = "Operator-facing configuration options for the application"; type = deferredModuleWith { staticModules = [ { _class = "fediversity-application-config"; } ]; }; }; implementation = mkOption { description = "Mapping of application configuration to deployment resources, a description of what an application needs to run"; type = application.config.config-mapping.function-type; }; resources = mkOption { description = "Compute resources required by an application"; type = application.config.config-mapping.function-type; readOnly = true; default = application.config.config-mapping.apply; }; # TODO(@fricklerhandwerk): this needs a better name config-mapping = mkOption { description = "Function type for the mapping from application configuration to required resources"; type = functionType; readOnly = true; default = { input-type = submodule application.config.module; output-type = application-resources; implementation = application.config.implementation; }; }; }; }) ); }; 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 = functionType; readOnly = true; default = { input-type = submodule { options = { deployment-name = mkOption { type = types.str; }; required-resources = mkOption { type = attrsOf application-resources; }; }; }; output-type = deployment-type; implementation = environment.config.implementation; }; }; config-mapping = mkOption { description = "Mapping from a configuration to a deployment"; type = functionType; readOnly = true; default = { input-type = submodule { options = { deployment-name = mkOption { type = types.str; }; configuration = mkOption { type = config.configuration; }; }; }; output-type = deployment-type; implementation = { deployment-name, configuration, }: # TODO: check cfg.enable.true let required-resources = lib.mapAttrs ( name: application-settings: config.applications.${name}.resources application-settings ) configuration.applications; in environment.config.resource-mapping.apply { inherit required-resources deployment-name; }; }; }; # 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 = environment.config.config-mapping.function-type; readOnly = true; default = environment.config.config-mapping.apply; }; }; }) ); }; configuration = mkOption { description = "Configuration type declaring options to be set by operators"; type = optionType; readOnly = true; default = submodule { options = { enable = lib.mkEnableOption "your Fediversity configuration"; applications = lib.mapAttrs ( _name: application: mkOption { description = application.description; type = submodule application.module; default = { }; } ) config.applications; }; }; }; }; }