{ pkgs, lib, sources ? import ../../npins, inputs ? null, ... }: # FIXME allow custom deployment types # FIXME make deployments environment resources? let inherit (lib) mkOption types; inherit (lib.types) int nullOr raw str submodule ; inherit (pkgs.callPackage ../utils.nix { }) mapKeys withPackages withEnv withSecrets tfApply ; 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; } '' ); 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; }; httpBackend = mkOption { description = "environment variables to configure the TF HTTP back-end, see "; # type = types.attrsOf (types.either types.str types.int); type = types.submodule (http-backend: { options = { value = mkOption { readOnly = true; default = lib.mapAttrs' (k: v: lib.nameValuePair "TF_HTTP_${lib.toUpper k}" (builtins.toString v)) { inherit (http-backend.config) address update_method lock_address lock_method unlock_address unlock_method username password skip_cert_verification retry_max retry_wait_min retry_wait_max ; }; }; address = mkOption { description = "The address of the REST endpoint"; type = str; }; update_method = mkOption { description = "HTTP method to use when updating state."; type = str; default = "POST"; }; lock_address = mkOption { description = "The address of the lock REST endpoint."; type = str; default = http-backend.config.address; }; lock_method = mkOption { description = "The HTTP method to use when locking."; type = str; default = "LOCK"; }; unlock_address = mkOption { description = "The address of the unlock REST endpoint."; type = str; default = http-backend.config.address; }; unlock_method = mkOption { description = "The HTTP method to use when unlocking."; type = str; default = "UNLOCK"; }; username = mkOption { description = "The username for HTTP basic authentication."; type = str; default = "basic"; }; password = mkOption { description = "The password for HTTP basic authentication."; type = str; default = "fake-secret"; }; skip_cert_verification = mkOption { description = "Whether to skip TLS verification."; type = str; default = "false"; }; retry_max = mkOption { description = "The number of HTTP request retries."; type = types.int; default = 2; }; retry_wait_min = mkOption { description = "The minimum time in seconds to wait between HTTP request attempts."; type = types.int; default = 1; }; retry_wait_max = mkOption { description = "The maximum time in seconds to wait between HTTP request attempts."; type = types.int; default = 30; }; }; }); }; 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"; }; }; }; }; in { 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; 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 ${withEnv 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 httpBackend 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; }; run = mkOption { type = types.package; readOnly = true; default = let inherit (tf-host.config) system ssh caller args deployment-name root-path httpBackend ; inherit (ssh) host username key-file sshOpts ; in tfApply { inherit httpBackend; directory = "tf-single-host"; 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-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 httpBackend nixos-configuration; ssh = host-ssh; node-name = mkOption { description = "the name of the ProxmoX node to use."; type = types.str; }; imageDatastoreId = mkOption { description = "ID of the datastore of the image."; type = types.str; default = "local"; }; run = mkOption { type = types.package; 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 = "fediversity-template"; # 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; }; in lib.trace (lib.strings.toJSON environment) pkgs.writers.writeBashBin "deploy-tf-proxmox-template.sh" (withPackages [ pkgs.qemu ]) '' 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 checksum="$(sha256sum /tmp/${name}.qcow2 | cut -d " " -f1)" env \ TF_VAR_image=/tmp/${name}.qcow2 \ TF_VAR_checksum="$checksum" \ ${lib.getExe (tfApply { inherit httpBackend environment; directory = "tf-proxmox-template"; })} ''; }; }; }); }; 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 httpBackend 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; }; 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.nullOr 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; 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; }; in lib.trace (lib.strings.toJSON environment) (tfApply { inherit httpBackend environment; directory = "tf-proxmox-vm"; dependentDirs = [ "tf-single-host" ]; }); }; }; }); }; tf-netbox-store-ips = mkOption { description = "Store a range of IPs in a Netbox instance."; type = submodule (tf-netbox-store-ips: { options = { inherit httpBackend; startAddress = mkOption { description = "Start of the IP range."; type = types.str; example = "10.0.0.1/24"; }; endAddress = mkOption { description = "End of the IP range."; type = types.str; example = "10.0.0.50/24"; }; run = mkOption { type = types.package; readOnly = true; default = let inherit (tf-netbox-store-ips.config) httpBackend startAddress endAddress ; in tfApply { inherit httpBackend; directory = "tf-netbox-store-ips"; environment = { start_address = startAddress; end_address = endAddress; }; }; }; }; }); }; tf-netbox-get-ip = mkOption { description = "Get an available IP from a Netbox instance."; type = submodule (tf-netbox-get-ip: { options = { inherit httpBackend; run = mkOption { type = types.package; readOnly = true; default = let inherit (tf-netbox-get-ip.config) httpBackend ; in tfApply { inherit httpBackend; directory = "tf-netbox-get-ip"; environment = { }; }; }; }; }); }; octodns-zone = mkOption { description = "Manage DNS records."; type = submodule ( octodns-zone: let dns = pkgs.callPackage sources."dns.nix" { }; in { options = { domain = mkOption { type = types.str; example = "example.tld"; }; secretFiles = mkOption { type = types.attrsOf types.str; description = "The files from which to read the secrets to use with the provider."; example = { token = "/path/to/token"; }; }; zone = mkOption { # FIXME: error: The option `zones."domain.tld".__toString' is read-only, but it's set multiple times. # type = dns.lib.types.zone; description = "The zone data to use."; example = { NS = [ "ns.example.tld." ]; A = [ "12.34.56.78" ]; }; }; provider = mkOption { type = types.str; description = "The OctoDNS provider to use, see ."; example = "powerdns"; }; providers = mkOption { type = types.attrsOf ( types.submodule { options = { class = mkOption { type = types.str; }; secrets = mkOption { type = types.listOf types.str; }; configuration = mkOption { # FIXME how do i express `attrsOf option` if there is no `types.option`? type = types.attrsOf types.raw; default = { }; }; }; } ); default = { desec = { class = "octodns_desec.DesecProvider"; secrets = [ "token" ]; }; hetzner = { class = "octodns_hetzner.HetznerProvider"; secrets = [ "token" ]; }; powerdns = { class = "octodns_powerdns.PowerDnsProvider"; secrets = [ "api_key" ]; configuration = { host = mkOption { type = str; description = "The host on which PowerDNS api is listening."; }; port = mkOption { type = int; description = "The port on which PowerDNS api is listening."; default = 8081; }; }; }; }; }; configuration = mkOption { type = submodule { options = octodns-zone.config.providers.${octodns-zone.config.provider}.configuration; }; default = { }; }; package = mkOption { type = types.package; example = "The package of the OctoDNS provider to deploy to, see ."; default = pkgs.octodns-providers.${octodns-zone.config.provider}; }; packages = mkOption { type = types.listOf types.package; default = [ # https://github.com/NixOS/nixpkgs/issues/429294 ( (pkgs.callPackage "${sources.nixpkgs-unstable}/pkgs/by-name/oc/octodns/package.nix" { }) .withProviders (_: [ pkgs.octodns-providers.bind octodns-zone.config.package ]) ) ]; }; conf = mkOption { type = types.path; default = let inherit (octodns-zone.config) domain zone providers provider ; in pkgs.writers.writeYAML "octodns-config.yaml" { zones."${domain}." = { sources = [ "config" ]; targets = [ provider ]; }; providers = { "${provider}" = let inherit (providers."${provider}") class secrets; in { inherit class; } // octodns-zone.config.configuration // (lib.genAttrs secrets (k: "env/${lib.toUpper "${provider}_${k}"}")); config = { file_extension = ""; class = "octodns_bind.ZoneFileSource"; directory = pkgs.linkFarm "zones" { "${domain}" = dns.util.writeZone domain ( # lib.recursiveUpdate { # fake SOA record to satisfy octodns SOA = { serial = 1970010100; nameServer = "ns1.example.com"; adminEmail = "admin@example.com"; }; } // zone ); }; }; }; }; }; validate = mkOption { type = types.package; default = let inherit (octodns-zone.config) packages conf provider secretFiles ; in pkgs.writers.writeBashBin "octodns-validate.sh" (withPackages packages) '' env ${withSecrets (mapKeys (k: lib.toUpper "${provider}_${k}") secretFiles)} \ octodns-validate --config ${conf} --all ''; }; sync = mkOption { type = types.package; default = let inherit (octodns-zone.config) packages conf provider secretFiles ; in pkgs.writers.writeBashBin "octodns-sync.sh" (withPackages packages) '' env ${withSecrets (mapKeys (k: lib.toUpper "${provider}_${k}") secretFiles)} \ octodns-sync --config ${conf} --doit ''; }; }; } ); }; }