From fd43664e7c4d657a367f3d7fd3666d85f8ca35c4 Mon Sep 17 00:00:00 2001 From: magic_rb Date: Mon, 20 Apr 2026 23:46:27 +0200 Subject: [PATCH] Convert `on-machine` backend from Nix-Bash into Jq-Bash I don't know which is worse, but I prefer the latter. Signed-off-by: magic_rb --- backends/on-machine.nix | 149 ----------------------- backends/on-machine/default.nix | 58 +++++++++ backends/on-machine/generate-secrets | 176 +++++++++++++++++++++++++++ flake.nix | 2 +- testing.nix | 2 +- 5 files changed, 236 insertions(+), 151 deletions(-) delete mode 100644 backends/on-machine.nix create mode 100644 backends/on-machine/default.nix create mode 100644 backends/on-machine/generate-secrets diff --git a/backends/on-machine.nix b/backends/on-machine.nix deleted file mode 100644 index df072b0..0000000 --- a/backends/on-machine.nix +++ /dev/null @@ -1,149 +0,0 @@ -# we use this vars backend as an example backend. -# this generates a script which creates the values at the expected path. -# this script has to be run manually (I guess after updating the system) to generate the required vars. -{ - pkgs, - lib, - config, - ... -}: -let - cfg = config.vars.settings.on-machine; - sortedGenerators = - (lib.toposort (a: b: builtins.elem a.name b.dependencies) (lib.attrValues config.vars.generators)) - .result; - - promptCmd = { - hidden = "read -sr prompt_value"; - line = "read -r prompt_value"; - multiline = '' - echo 'press control-d to finish' - prompt_value=$(cat) - ''; - }; - generate-vars = pkgs.writeShellApplication { - name = "generate-vars"; - text = '' - set -efuo pipefail - - PATH=${lib.makeBinPath [ pkgs.coreutils ]} - - # make the output directory overridable - OUT_DIR=''${OUT_DIR:-${cfg.fileLocation}} - - # check if all files are present or all files are missing - # if not, they are in an inconsistent state and we bail out - ${lib.concatMapStringsSep "\n" (gen: '' - all_files_missing=true - all_files_present=true - echo "Checking vars for ${gen.name}..." - ${lib.concatMapStringsSep "\n" (file: '' - OUT_FILE="$OUT_DIR"/${if file.secret then "secret" else "public"}/${file.generator}/${file.name} - if test -e "$OUT_FILE"; then - all_files_missing=false - else - all_files_present=false - fi - '') (lib.attrValues gen.files)} - - # outputs - out=$(mktemp -d) - trap 'rm -rf $out' EXIT - export out - mkdir -p "$out" - - if [ $all_files_missing = false ] && [ $all_files_present = false ] ; then - echo "Inconsistent state for generator: ${gen.name}" - exit 1 - fi - if [ $all_files_present = true ] ; then - echo "All secrets for ${gen.name} are present" - elif [ $all_files_missing = true ] ; then - - # prompts - prompts=$(mktemp -d) - trap 'rm -rf $prompts' EXIT - export prompts - mkdir -p "$prompts" - ${lib.concatMapStringsSep "\n" (prompt: '' - echo ${lib.escapeShellArg prompt.description} - ${promptCmd.${prompt.type}} - echo -n "$prompt_value" > "$prompts"/${prompt.name} - '') (lib.attrValues gen.prompts)} - echo "Generating vars for ${gen.name}" - - # dependencies - in=$(mktemp -d) - trap 'rm -rf $in' EXIT - export in - mkdir -p "$in" - ${lib.concatMapStringsSep "\n" (input: '' - mkdir -p "$in"/${input} - ${lib.concatMapStringsSep "\n" (file: '' - cp "$OUT_DIR"/${ - if file.secret then "secret" else "public" - }/${input}/${file.name} "$in"/${input}/${file.name} - '') (lib.attrValues config.vars.generators.${input}.files)} - '') gen.dependencies} - - ( - # prepare PATH - unset PATH - ${lib.optionalString (gen.runtimeInputs != [ ]) '' - PATH=${lib.makeBinPath gen.runtimeInputs} - export PATH - ''} - - # actually run the generator - ${gen.script} - ) - - # check if all files got generated - ${lib.concatMapStringsSep "\n" (file: '' - if ! test -e "$out"/${file.name} ; then - echo 'generator ${gen.name} failed to generate ${file.name}' - exit 1 - fi - '') (lib.attrValues gen.files)} - - # move the files to the correct location - ${lib.concatMapStringsSep "\n" (file: '' - OUT_FILE="$OUT_DIR"/${if file.secret then "secret" else "public"}/${file.generator}/${file.name} - mkdir -p "$(dirname "$OUT_FILE")" - mv "$out"/${file.name} "$OUT_FILE" - '') (lib.attrValues gen.files)} - fi - - # move the files to the correct location - ${lib.concatMapStringsSep "\n" (file: '' - OUT_FILE="$OUT_DIR"/${if file.secret then "secret" else "public"}/${file.generator}/${file.name} - chown ${file.owner}:${file.group} "''${OUT_FILE}" - chmod ${file.mode} "''${OUT_FILE}" - '') (lib.attrValues gen.files)} - rm -rf "$out" - '') sortedGenerators} - ''; - }; -in -{ - options.vars.settings.on-machine = { - enable = lib.mkEnableOption "Enable on-machine vars backend"; - fileLocation = lib.mkOption { - type = lib.types.str; - default = "/etc/vars"; - }; - }; - config = lib.mkIf cfg.enable { - vars.settings.fileModule = file: { - path = - if file.config.secret then - "${cfg.fileLocation}/secret/${file.config.generator}/${file.config.name}" - else - "${cfg.fileLocation}/public/${file.config.generator}/${file.config.name}"; - }; - environment.systemPackages = [ - generate-vars - ]; - system.build.generate-vars = generate-vars; - }; -} diff --git a/backends/on-machine/default.nix b/backends/on-machine/default.nix new file mode 100644 index 0000000..d044c11 --- /dev/null +++ b/backends/on-machine/default.nix @@ -0,0 +1,58 @@ +# we use this vars backend as an example backend. +# this generates a script which creates the values at the expected path. +# this script has to be run manually (I guess after updating the system) to generate the required vars. +{ + pkgs, + lib, + config, + ... +}: let + cfg = config.vars.settings.on-machine; + sortedGenerators = + (lib.toposort (a: b: builtins.elem a.name b.dependencies) (lib.attrValues config.vars.generators)) + .result; + + promptCmd = { + hidden = "read -sr prompt_value"; + line = "read -r prompt_value"; + multiline = '' + echo 'press control-d to finish' + prompt_value=$(cat) + ''; + }; + generate-vars = pkgs.writeShellApplication { + name = "generate-vars"; + runtimeInputs = with pkgs; [ + coreutils + jq + ]; + text = '' + _config=${pkgs.writers.writeJSON "generate-vars.json" { + file_location = cfg.fileLocation; + generators = sortedGenerators; + }} + + ${builtins.readFile ./generate-secrets} + ''; + }; +in { + options.vars.settings.on-machine = { + enable = lib.mkEnableOption "Enable on-machine vars backend"; + fileLocation = lib.mkOption { + type = lib.types.str; + default = "/etc/vars"; + }; + }; + config = lib.mkIf cfg.enable { + vars.settings.fileModule = file: { + path = + if file.config.secret + then "${cfg.fileLocation}/secret/${file.config.generator}/${file.config.name}" + else "${cfg.fileLocation}/public/${file.config.generator}/${file.config.name}"; + }; + environment.systemPackages = [ + generate-vars + ]; + system.build.generate-vars = generate-vars; + }; +} diff --git a/backends/on-machine/generate-secrets b/backends/on-machine/generate-secrets new file mode 100644 index 0000000..5005b6b --- /dev/null +++ b/backends/on-machine/generate-secrets @@ -0,0 +1,176 @@ +# -*- mode: shell-script -*- + +set -efuo pipefail + +function _generators() { + jq --raw-output '.generators | map(.name) | .[]' < "$_config" +} + +function _generator_files() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .files | map(.name) | .[]' < "$_config" +} + +function _generator_file_get() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .files.[] | select(.name == "'"$2"'") | .'"$3" < "$_config" +} + +function _generator_prompts() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .prompts | map(.name) | .[]' < "$_config" +} + +function _generator_prompt_get() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .prompts.[] | select(.name == "'"$2"'") | .'"$3" < "$_config" +} + +function _generator_dependencies() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .dependencies[]' < "$_config" +} + +function _generator_runtime_inputs() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .runtimeInputs[]' < "$_config" +} + +function _generator_script() { + jq --raw-output '.generators.[] | select(.name == "'"$1"'") | .script' < "$_config" +} + +function _file_location() { + jq --raw-output '.file_location' < "$_config" +} + +# make the output directory overridable +OUT_DIR="${OUT_DIR:-$(_file_location)}" + +# check if all files are present or all files are missing +# if not, they are in an inconsistent state and we bail out +while read -u 10 -r _generator_name ; do + all_files_missing=true + all_files_present=true + echo "Checking vars for ${_generator_name}..." + while read -r _file_name ; do + _file_generator="$(_generator_file_get "$_generator_name" "$_file_name" "generator")" + + if [[ "$(_generator_file_get "$_generator_name" "$_file_name" "secret")" == "true" ]] ; then + OUT_FILE="$OUT_DIR/secret/${_file_generator}/${_file_name}" + else + OUT_FILE="$OUT_DIR/public/${_file_generator}/${_file_name}" + fi + + if test -e "$OUT_FILE"; then + all_files_missing=false + else + all_files_present=false + fi + done < <(_generator_files "$_generator_name") + + # outputs + out="$(mktemp -d)" + trap 'rm -rf $out' EXIT + export out + mkdir -p "$out" + + if [[ "$all_files_missing" == false ]] && [[ "$all_files_present" == false ]] ; then + echo "Inconsistent state for generator: ${_generator_name}" + exit 1 + fi + if [[ "$all_files_present" = true ]] ; then + echo "All secrets for ${_generator_name} are present" + elif [[ "$all_files_missing" = true ]] ; then + # prompts + prompts="$(mktemp -d)" + trap 'rm -rf $prompts' EXIT + export prompts + mkdir -p "$prompts" + while read -u 11 -r _prompt_name ; do + _prompt_description="$(_generator_prompt_get "$_generator_name" "$_prompt_name" "description")" + _prompt_type="$(_generator_prompt_get "$_generator_name" "$_prompt_name" "type")" + + printf "%s" "$_prompt_description" + + case "$_prompt_type" in + hidden) + read -sr prompt_value + ;; + line) + read -r prompt_value + ;; + multiline) + echo 'press control-d to finish' + prompt_value="$(cat)" + ;; + *) + echo "unknown prompt type $_prompt_type" + exit 1 + esac + + echo -n "$prompt_value" > "$prompts/${_prompt_name}" + done 11< <(_generator_prompts "$_generator_name") + + echo "Generating vars for ${_generator_name}" + + # dependencies + in=$(mktemp -d) + trap 'rm -rf $in' EXIT + export in + mkdir -p "$in" + + while read -r _dependency ; do + mkdir -p "$in/${_dependency}" + while read -r _file_name ; do + if [[ "$(_generator_file_get "$_dependency" "$_file_name" "secret")" == "true" ]] ; then + cp "$OUT_DIR/secret/$_dependency/$_file_name" "$in/$_dependency/$_file_name" + else + cp "$OUT_DIR/public/$_dependency/$_file_name" "$in/$_dependency/$_file_name" + fi + done < <(_generator_files "$_dependency") + done < <(_generator_dependencies "$_generator_name") + + # prepare PATH + NEW_PATH="" + while read -r _runtime_input ; do + NEW_PATH="${NEW_PATH:+$NEW_PATH:}$_runtime_input/bin" + done < <(_generator_runtime_inputs "$_generator_name") + + # actually run the generator + PATH="$NEW_PATH" "$(command -v bash)" <<<"$(_generator_script "$_generator_name")" + + # check if all files got generated + while read -r _file_name ; do + if ! [[ -e "$out/${_file_name}" ]] ; then + echo "generator ${_generator_name} failed to generate ${_file_name}" + exit 1 + fi + done < <(_generator_files "$_generator_name") + + + # move the files to the correct location + while read -r _file_name ; do + if [[ "$(_generator_file_get "$_generator_name" "$_file_name" "secret")" == "true" ]] ; then + OUT_FILE="$OUT_DIR/secret/${_file_generator}/${_file_name}" + else + OUT_FILE="$OUT_DIR/public/${_file_generator}/${_file_name}" + fi + + mkdir -p "$(dirname "$OUT_FILE")" + mv "$out/${_file_name}" "$OUT_FILE" + done < <(_generator_files "$_generator_name") + + # move the files to the correct location + while read -r _file_name ; do + if [[ "$(_generator_file_get "$_generator_name" "$_file_name" "secret")" == "true" ]] ; then + OUT_FILE="$OUT_DIR/secret/${_file_generator}/${_file_name}" + else + OUT_FILE="$OUT_DIR/public/${_file_generator}/${_file_name}" + fi + + _file_owner="$(_generator_file_get "$_generator_name" "$_file_name" "owner")" + _file_group="$(_generator_file_get "$_generator_name" "$_file_name" "group")" + _file_mode="$(_generator_file_get "$_generator_name" "$_file_name" "mode")" + + chown "${_file_owner}:${_file_group}" "${OUT_FILE}" + chmod "${_file_mode}" "${OUT_FILE}" + done < <(_generator_files "$_generator_name") + + rm -rf "$out" + fi +done 10< <(_generators) diff --git a/flake.nix b/flake.nix index 1181eae..3e13494 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,7 @@ forAllSystems = lib.genAttrs supportedSystems; in { nixosModules.default = { imports = [ ./options.nix ]; }; - nixosModules.backend-on-machine = { imports = [ ./backends/on-machine.nix ]; }; + nixosModules.backend-on-machine = { imports = [ ./backends/on-machine ]; }; checks = forAllSystems (system: let tests = { testing = inputs.nixpkgs.lib.nixos.runTest { diff --git a/testing.nix b/testing.nix index 62bcf5e..ca3ade7 100644 --- a/testing.nix +++ b/testing.nix @@ -9,7 +9,7 @@ { imports = [ ./options.nix - ./backends/on-machine.nix + ./backends/on-machine ]; vars.settings.on-machine.enable = true; environment.systemPackages = [