From 0630e74e6f0c463f1b27960bfd6d90a5640af71c Mon Sep 17 00:00:00 2001 From: Kiara Grouwstra Date: Sun, 10 Aug 2025 12:29:40 +0200 Subject: [PATCH] add tf --- deployment/data-model.nix | 4 ++ infra/dev/main.nix | 38 +++++++++++++++ infra/sync-nix/main.tf | 91 ++++++++++++++++++++++++++++++++++++ infra/sync-nix/nixos.nix | 14 ++++++ infra/sync-nix/variables.nix | 27 +++++++++++ infra/sync-nix/variables.tf | 17 +++++++ 6 files changed, 191 insertions(+) create mode 100644 infra/dev/main.nix create mode 100644 infra/sync-nix/main.tf create mode 100644 infra/sync-nix/nixos.nix create mode 100644 infra/sync-nix/variables.nix create mode 100644 infra/sync-nix/variables.tf diff --git a/deployment/data-model.nix b/deployment/data-model.nix index dd7ada19..cbf921d0 100644 --- a/deployment/data-model.nix +++ b/deployment/data-model.nix @@ -26,6 +26,10 @@ let }; }; deployment = attrTag { + tf-ssh = mkOption { + description = "A Terraform deployment by SSH to update a single existing NixOS host"; + type = types.attrset; + }; }; in { diff --git a/infra/dev/main.nix b/infra/dev/main.nix new file mode 100644 index 00000000..4579d236 --- /dev/null +++ b/infra/dev/main.nix @@ -0,0 +1,38 @@ +let + vm_domain = "abundos.eu"; +in +{ + module."nixos" = + builtins.mapAttrs + (service: hostname: { + source = "../sync-nix"; + inherit vm_domain hostname; + config_tf = { + terraform = { + inherit hostname; + domain = vm_domain; + }; + }; + config_nix = '' + { + imports = [ + # shared NixOS config + $${path.root}/../common/shared.nix + # FIXME: separate template options by service + $${path.root}/options.nix + # for service `forgejo` import `forgejo.nix` + $${path.root}/../../machines/dev/${hostname}/${service}.nix + # FIXME: get VM details from TF + $${path.root}/../../machines/dev/${hostname} + ]; + } + ''; + }) + { + # wiki = "vm02187" # does not resolve + # forgejo = "vm02116" # does not resolve + # TODO: move these to a separate `host` dir + dns = "fedi200"; + # fedipanel = "fedi201"; + }; +} diff --git a/infra/sync-nix/main.tf b/infra/sync-nix/main.tf new file mode 100644 index 00000000..dc140b55 --- /dev/null +++ b/infra/sync-nix/main.tf @@ -0,0 +1,91 @@ +locals { + system = "x86_64-linux" + # dependency paths pre-calculated from npins + pins = jsondecode(file("${path.module}/.npins.json")) + # nix path: expose pins, use nixpkgs in flake commands (`nix run`) + nix_path = "${join(":", [for name, dir in local.pins : "${name}=${dir}"])}:flake=${local.pins["nixpkgs"]}:flake" +} + +# hash of our code directory, used to trigger re-deploy +# FIXME calculate separately to reduce false positives +data "external" "hash" { + program = ["sh", "-c", "echo \"{\\\"hash\\\":\\\"$(nix-hash ..)\\\"}\""] +} + +# TF resource to build and deploy NixOS instances. +resource "terraform_data" "nixos" { + + # trigger rebuild/deploy if (FIXME?) any potentially used config/code changed, + # preventing these (20+s, build being bottleneck) when nothing changed. + # terraform-nixos separates these to only deploy if instantiate changed, + # yet building even then - which may be not as bad using deploy on remote. + # having build/deploy one resource reflects wanting to prevent no-op rebuilds + # over preventing (with less false positives) no-op deployments, + # as i could not find a way to do prevent no-op rebuilds without merging them: + # - generic resources cannot have outputs, while we want info from the instantiation (unless built on host?). + # - `data` always runs, which is slow for deploy and especially build. + triggers_replace = [ + data.external.hash.result, + var.hostname, + var.config_nix, + var.config_tf, + ] + + provisioner "local-exec" { + # directory to run the script from. we use the TF project root dir, + # here as a path relative from where TF is run from, + # matching calling modules' expectations on config_nix locations. + # note that absolute paths can cause false positives in triggers, + # so are generally discouraged in TF. + working_dir = path.root + environment = { + # nix path used on build, lets us refer to e.g. nixpkgs like `` + NIX_PATH = local.nix_path + } + # TODO: refactor back to command="ignoreme" interpreter=concat([]) to protect sensitive data from error logs? + # TODO: build on target? + command = <<-EOF + set -euo pipefail + + # INSTANTIATE + command=( + nix-instantiate + --expr + 'import ./nixos.nix { + system = "${local.system}"; + configuration = + ${var.config_nix} // + builtins.fromJSON "${replace(jsonencode(var.config_tf), "\"", "\\\"")}" // + { + nix.nixPath = [ "${local.nix_path}" ]; + }; + }' + ) + # instantiate the config in /nix/store + "$${command[@]}" -A out_path + # get the other info + json="$("$${command[@]}" --eval --strict --json)" + + # DEPLOY + declare substituters trusted_public_keys drv_path + # set our variables using the json object + eval "export $(echo $json | jaq -r 'to_entries | map("\(.key)=\(.value)") | @sh')" + host="root@${var.hostname}.${var.vm_domain}" # FIXME: #24 + buildArgs=( + --option extra-binary-caches https://cache.nixos.org/ + --option substituters $substituters + --option trusted-public-keys $trusted_public_keys + ) + sshOpts=( + -o BatchMode=yes + -o StrictHostKeyChecking=no + ) + # get the realized derivation to deploy + outPath=$(nix-store --realize "$drv_path" "$${buildArgs[@]}") + # 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 + ssh "$${sshOpts[@]}" "$host" "nix-env --profile /nix/var/nix/profiles/system --set $outPath; $outPath/bin/switch-to-configuration switch" + EOF + } +} diff --git a/infra/sync-nix/nixos.nix b/infra/sync-nix/nixos.nix new file mode 100644 index 00000000..b8234573 --- /dev/null +++ b/infra/sync-nix/nixos.nix @@ -0,0 +1,14 @@ +{ + configuration, + system ? builtins.currentSystem, +}: +let + sources = import ../../npins; + os = import "${sources.nixpkgs}/nixos" { inherit system configuration; }; +in +{ + substituters = builtins.concatStringsSep " " os.config.nix.settings.substituters; + trusted_public_keys = builtins.concatStringsSep " " os.config.nix.settings.trusted-public-keys; + drv_path = os.config.system.build.toplevel.drvPath; + out_path = os.config.system.build.toplevel; +} diff --git a/infra/sync-nix/variables.nix b/infra/sync-nix/variables.nix new file mode 100644 index 00000000..db08fdcc --- /dev/null +++ b/infra/sync-nix/variables.nix @@ -0,0 +1,27 @@ +# TODO: generate nix module from `variables.tf` +# - TF -> JSON schema: https://melvinkoh.me/parsing-terraform-for-forms-clr4zq4tu000309juab3r1lf7 +# - python313Packages.python-hcl2: hcl2tojson variables.tf +# - JSON schema -> nix +{ lib, ... }: +let + inherit (lib) mkOption types; + inherit (types) string attrsOf any; +in +{ + options = { + vm_domain = mkOption { + type = string; + }; + hostname = mkOption { + type = string; + }; + config_nix = mkOption { + type = string; + default = { }; + }; + config_tf = mkOption { + type = attrsOf any; + default = { }; + }; + }; +} diff --git a/infra/sync-nix/variables.tf b/infra/sync-nix/variables.tf new file mode 100644 index 00000000..a090790e --- /dev/null +++ b/infra/sync-nix/variables.tf @@ -0,0 +1,17 @@ +variable "vm_domain" { + type = string +} + +variable "hostname" { + type = string +} + +variable "config_nix" { + type = string + default = "{}" +} + +variable "config_tf" { + type = map(any) + default = {} +}