Skip to content

Commit

Permalink
This PR adds a Terraform input variable named special_args. This al…
Browse files Browse the repository at this point in the history
…lows passing in a JSON string from Terraform to expose to NixOS's `specialArgs` at build-time.

This implementation extends the original `lib.nixosSystem` call to allow passing info without either use of `--impure` or having to stage to Git, thanks to @Mic92's suggestion at nix-community#414.

Example usage, in this case presuming deployment to a Hetzner Cloud server (`resource.hcloud_server`):

```nix
let
  servers = ...;
  variable = ...;
  data = ...;
  resource = ...;
in
{
  inherit variable data resource;
  module =
    lib.mapAttrs (server_name: _server_config: let
    in {
      # pin module version by nix flake inputs
      source =
"github.com/numtide/nixos-anywhere?ref=${inputs.nixos-anywhere.sourceInfo.rev}/terraform/all-in-one";
      ...
      special_args = lib.tfRef "jsonencode(${lib.strings.toJSON {
        tf = {
          inherit server_name;
          # all variables
          # var = lib.mapAttrs (k: _: lib.tfRef "var.${k}") variable;
          # non-sensitive variables
          var = lib.mapAttrs (k: _: lib.tfRef "var.${k}")
  (lib.filterAttrs (_k: v: !(v ? sensitive && v.sensitive)) variable);
          data = lib.mapAttrs (type: instances: lib.mapAttrs (k: _:
  tfRef "data.${type}.${k}") instances) data;
          resource = lib.mapAttrs (type: instances: lib.mapAttrs (k:
  _: tfRef "resource.${type}.${k}") instances) resource;
          server = lib.tfRef "resource.hcloud_server.${server_name}";
        };
      }})";
    })
    servers;
}
```

You can then use these in your `nixosConfigurations`, in this example thru the `tf` argument.

This implementation differs from my previous attempts in neither being impure, nor adding files.
An advantage of this is it is a relatively simple implementation that should work while getting out of your way.
An [alternative design](nix-community/nixos-anywhere@main...KiaraGrouwstra:nixos-anywhere:tf-info-to-wrapper#diff-2e2429dde4812f0b50c784e8d4c8b93cc9faeb52cce4747733200f65ea5c2bbb) suggested by @Mic92 involved passing the information not directly, but rather thru a file. The idea would be that this might help reduce the risk of stack overflows, tho I have imagined (perhaps naively) that TF info has tended not to get too big, whereas I also had a bit more trouble getting that approach to work properly so far (involving both NARs that would suddenly mismatch again, while I'd also yet to test if one could put such files in `.gitignore`).

As a note on security, information passed this way _will_ hit `/nix/store/`. As such, the above usage example has defaulted to omitting TF variables marked as sensitive.

Note that, while from a UX perspective I would have preferred to allow directly passing nested structures without serializing, TF seemed to allow passing variables just as string or as map of strings - in which case I figured, if we have to manually serialize anyway, it would be a nicer experience to just serialize once (to a string-type variable) than to do so for each special argument (to use a map-of-strings TF variable).
  • Loading branch information
KiaraGrouwstra committed Oct 31, 2024
1 parent 51d347d commit 05c2e4f
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 11 deletions.
1 change: 1 addition & 0 deletions terraform/all-in-one.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ No resources.
| <a name="input_nixos_system_attr"></a> [nixos\_system\_attr](#input_nixos_system_attr) | The nixos system to deploy i.e. your-flake#nixosConfigurations.your-evaluated-nixos.config.system.build.toplevel or just your-evaluated-nixos.config.system.build.toplevel if you are not using flakes | `string` | n/a | yes |
| <a name="input_no_reboot"></a> [no\_reboot](#input_no_reboot) | DEPRECATED: Use `phases` instead. Do not reboot after installation | `bool` | `false` | no |
| <a name="input_phases"></a> [phases](#input_phases) | Phases to run. See `nixos-anywhere --help` for more information | `set(string)` | <pre>[<br> "kexec",<br> "disko",<br> "install",<br> "reboot"<br>]</pre> | no |
| <a name="input_special_args"></a> [special\_args](#input_special_args) | A map exposed as NixOS's `specialArgs` thru a file. | `string` | `"{}"` | no |
| <a name="input_stop_after_disko"></a> [stop\_after\_disko](#input_stop_after_disko) | DEPRECATED: Use `phases` instead. Exit after disko formatting | `bool` | `false` | no |
| <a name="input_target_host"></a> [target\_host](#input_target_host) | DNS host to deploy to | `string` | n/a | yes |
| <a name="input_target_port"></a> [target\_port](#input_target_port) | SSH port used to connect to the target\_host after installing NixOS. If install\_port is not set than this port is also used before installing. | `number` | `22` | no |
Expand Down
2 changes: 2 additions & 0 deletions terraform/all-in-one/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ module "system-build" {
attribute = var.nixos_system_attr
file = var.file
nix_options = var.nix_options
special_args = var.special_args
}

module "partitioner-build" {
source = "../nix-build"
attribute = var.nixos_partitioner_attr
file = var.file
nix_options = var.nix_options
special_args = var.special_args
}

locals {
Expand Down
6 changes: 6 additions & 0 deletions terraform/all-in-one/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ variable "nixos_facter_path" {
description = "Path to which to write a `facter.json` generated by `nixos-facter`."
default = ""
}

variable "special_args" {
type = string
default = "{}"
description = "A map exposed as NixOS's `specialArgs` thru a file."
}
11 changes: 6 additions & 5 deletions terraform/nix-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ No modules.

## Inputs

| Name | Description | Type | Default | Required |
| ------------------------------------------------------------------- | -------------------------------------------------- | ------------- | ------- | :------: |
| <a name="input_attribute"></a> [attribute](#input_attribute) | the attribute to build, can also be a flake | `string` | n/a | yes |
| <a name="input_file"></a> [file](#input_file) | the nix file to evaluate, if not run in flake mode | `string` | `null` | no |
| <a name="input_nix_options"></a> [nix\_options](#input_nix_options) | the options of nix | `map(string)` | `{}` | no |
| Name | Description | Type | Default | Required |
| ---------------------------------------------------------------------- | --------------------------------------------------- | ------------- | ------- | :------: |
| <a name="input_attribute"></a> [attribute](#input_attribute) | the attribute to build, can also be a flake | `string` | n/a | yes |
| <a name="input_file"></a> [file](#input_file) | the nix file to evaluate, if not run in flake mode | `string` | `null` | no |
| <a name="input_nix_options"></a> [nix\_options](#input_nix_options) | the options of nix | `map(string)` | `{}` | no |
| <a name="input_special_args"></a> [special\_args](#input_special_args) | A map exposed as NixOS's `specialArgs` thru a file. | `string` | `"{}"` | no |

## Outputs

Expand Down
1 change: 1 addition & 0 deletions terraform/nix-build/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ data "external" "nix-build" {
attribute = var.attribute
file = var.file
nix_options = local.nix_options
special_args = var.special_args
}
}
output "result" {
Expand Down
31 changes: 25 additions & 6 deletions terraform/nix-build/nix-build.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
#!/usr/bin/env bash
set -efu

declare file attribute nix_options
eval "$(jq -r '@sh "attribute=\(.attribute) file=\(.file) nix_options=\(.nix_options)"')"
declare file attribute nix_options special_args
eval "$(jq -r '@sh "attribute=\(.attribute) file=\(.file) nix_options=\(.nix_options) special_args=\(.special_args)"')"
options=$(echo "${nix_options}" | jq -r '.options | to_entries | map("--option \(.key) \(.value)") | join(" ")')
if [[ -n ${file-} ]] && [[ -e ${file-} ]]; then
# shellcheck disable=SC2086
out=$(nix build --no-link --json $options -f "$file" "$attribute")
printf '%s' "$out" | jq -c '.[].outputs'
else
# shellcheck disable=SC2086
out=$(nix build --no-link --json $options "$attribute")
printf '%s' "$out" | jq -c '.[].outputs'
# pass the args in a pure fashion by extending the original config
if [[ ${special_args-} != "{}" ]]; then
rest="$(echo "${attribute}" | cut -d "#" -f 2)"
# e.g. config_path=nixosConfigurations.aarch64-linux.myconfig
config_path="${rest%.config.*}"
# e.g. config_attribute=config.system.build.toplevel
config_attribute="config.${rest#*.config.}"

# grab flake nar from error message
flake_rel="$(echo "${attribute}" | cut -d "#" -f 1)"
# e.g. flake_rel="."
flake_dir="$(readlink -f "${flake_rel}")"
flake_nar="$(nix build --expr "builtins.getFlake ''git+file://${flake_dir}?narHash=sha256-0000000000000000000000000000000000000000000=''" 2>&1 | grep -Po "(?<=got ')sha256-[^']*(?=')")"
# substitute variables into the template
nix_expr="(builtins.getFlake ''file://${flake_dir}/flake.nix?narHash=${flake_nar}'').${config_path}.extendModules { specialArgs = builtins.fromJSON ''${special_args}''; }"
# inject `special_args` into nixos config's `specialArgs`
# shellcheck disable=SC2086
out=$(nix build --no-link --json ${options} --expr "${nix_expr}" "${config_attribute}")
else
# shellcheck disable=SC2086
out=$(nix build --no-link --json ${options} "$attribute")
fi
fi
printf '%s' "$out" | jq -c '.[].outputs'
6 changes: 6 additions & 0 deletions terraform/nix-build/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ variable "nix_options" {
description = "the options of nix"
default = {}
}

variable "special_args" {
type = string
default = "{}"
description = "A map exposed as NixOS's `specialArgs` thru a file."
}

0 comments on commit 05c2e4f

Please sign in to comment.