Skip to content

Commit

Permalink
This PR adds a Terraform input variable named special_args.
Browse files Browse the repository at this point in the history
This allows passing in a map from Terraform to expose to NixOS's `specialArgs` at build-time.
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 = {
        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.

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.

This PR incorporates ideas from:

- @aanderse, who implemented a similar feature in [teraflops](https://github.com/aanderse/teraflops) that inspired this PR.
- @Mic92, who suggested (see #414) to extend the original `lib.nixosSystem` call to pass in info without `--impure` or staging to Git.
- @getchoo, who suggested getting the NAR hash by `nix flake prefetch` over `getFlake`, fixing an 'unlocked flake reference' error on (non-Lix) Nix.
- @threddast, who suggested to use TF's `any` type to automate serializing.

An [alternative design](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`).
  • Loading branch information
KiaraGrouwstra committed Nov 13, 2024
1 parent 51d347d commit d43e6f8
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 13 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. | `any` | `{}` | 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 = any
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. | `any` | `{}` | 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 = jsonencode(var.special_args)
}
}
output "result" {
Expand Down
40 changes: 32 additions & 8 deletions terraform/nix-build/nix-build.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
#!/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'
if [[ ${special_args-} == "{}" ]]; then
# no special arguments, proceed as normal
if [[ -n ${file-} ]] && [[ -e ${file-} ]]; then
# shellcheck disable=SC2086
out=$(nix build --no-link --json $options -f "$file" "$attribute")
else
# shellcheck disable=SC2086
out=$(nix build --no-link --json ${options} "$attribute")
fi
else
if [[ ${file-} != 'null' ]]; then
echo "special_args are currently only supported when using flakes!" >&2
exit 1
fi
# pass the args in a pure fashion by extending the original config
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 flake prefetch "${flake_dir}" --json | jq -r '.hash')"
# 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 "$attribute")
printf '%s' "$out" | jq -c '.[].outputs'
out=$(nix build --no-link --json ${options} --expr "${nix_expr}" "${config_attribute}")
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 = any
default = {}
description = "A map exposed as NixOS's `specialArgs` thru a file."
}

0 comments on commit d43e6f8

Please sign in to comment.