Fediversity/deployment/run/default.nix
Kiara Grouwstra c426cf2c55
add DNS zone by octodns
Signed-off-by: Kiara Grouwstra <kiara@procolix.eu>
2025-11-08 21:17:20 +01:00

824 lines
26 KiB
Nix

{
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 <https://developer.hashicorp.com/terraform/language/backend/http#configuration-variables>";
# 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 <https://octodns.readthedocs.io/en/latest/#providers>.";
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 <https://search.nixos.org/packages?channel=unstable&query=octodns-providers>.";
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
'';
};
};
}
);
};
}