init
This commit is contained in:
commit
edb187f514
6 changed files with 508 additions and 0 deletions
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# vars
|
||||||
|
|
||||||
|
This repo is for testing https://github.com/NixOS/nixpkgs/pull/370444 without patching your nixpkgs or running clan.
|
||||||
|
|
||||||
|
DavHau also wrote a blogpost about the more advanced version from clan.lol: https://clan.lol/blog/vars/
|
||||||
|
|
||||||
|
TODO: add more README
|
140
backends/on-machine.nix
Normal file
140
backends/on-machine.nix
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
# 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
|
||||||
|
${lib.concatMapStringsSep "\n" (file: ''
|
||||||
|
if test -e ${lib.escapeShellArg file.path} ; then
|
||||||
|
all_files_missing=false
|
||||||
|
else
|
||||||
|
all_files_present=false
|
||||||
|
fi
|
||||||
|
'') (lib.attrValues gen.files)}
|
||||||
|
|
||||||
|
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}
|
||||||
|
|
||||||
|
# outputs
|
||||||
|
out=$(mktemp -d)
|
||||||
|
trap 'rm -rf $out' EXIT
|
||||||
|
export out
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
(
|
||||||
|
# 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)}
|
||||||
|
rm -rf "$out"
|
||||||
|
fi
|
||||||
|
'') 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;
|
||||||
|
};
|
||||||
|
}
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1743583204,
|
||||||
|
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
32
flake.nix
Normal file
32
flake.nix
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
description = "testing vars without depending on clan";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
outputs = inputs:
|
||||||
|
let
|
||||||
|
lib = inputs.nixpkgs.lib;
|
||||||
|
supportedSystems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"i686-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"riscv64-linux"
|
||||||
|
];
|
||||||
|
forAllSystems = lib.genAttrs supportedSystems;
|
||||||
|
in {
|
||||||
|
nixosModules.default = { imports = [ ./options.nix ]; };
|
||||||
|
nixosModules.backend-on-machine = { imports = [ ./backends/on-machine.nix ]; };
|
||||||
|
# TODO fix tests
|
||||||
|
checks = forAllSystems (system: let
|
||||||
|
tests = {
|
||||||
|
testing = inputs.nixpkgs.lib.nixos.runTest {
|
||||||
|
hostPkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||||
|
imports = [
|
||||||
|
./options.nix
|
||||||
|
./testing.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in tests);
|
||||||
|
};
|
||||||
|
}
|
185
options.nix
Normal file
185
options.nix
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
options.vars = {
|
||||||
|
settings = {
|
||||||
|
fileModule = lib.mkOption {
|
||||||
|
type = lib.types.deferredModule;
|
||||||
|
internal = true;
|
||||||
|
description = ''
|
||||||
|
A module to be imported in every vars.files.<name> submodule.
|
||||||
|
Used by backends to define the `path` attribute.
|
||||||
|
|
||||||
|
Takes the file as an argument and returns maybe an attrset which should at least contain the `path` attribute.
|
||||||
|
Can be used to set other file attributes as well, like `value`.
|
||||||
|
'';
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
generators = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
A set of generators that can be used to generate files.
|
||||||
|
Generators are scripts that produce files based on the values of other generators and user input.
|
||||||
|
Each generator is expected to produce a set of files under a directory.
|
||||||
|
'';
|
||||||
|
default = { };
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (generator: {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.strMatching "[a-zA-Z0-9:_\\.-]*";
|
||||||
|
description = ''
|
||||||
|
The name of the generator.
|
||||||
|
This name will be used to refer to the generator in other generators.
|
||||||
|
'';
|
||||||
|
readOnly = true;
|
||||||
|
default = generator.config._module.args.name;
|
||||||
|
defaultText = "Name of the generator";
|
||||||
|
};
|
||||||
|
|
||||||
|
dependencies = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
A list of other generators that this generator depends on.
|
||||||
|
The output values of these generators will be available to the generator script as files.
|
||||||
|
For example, the file 'file1' of a dependency named 'dep1' will be available via $in/dep1/file1.
|
||||||
|
'';
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
files = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
A set of files to generate.
|
||||||
|
The generator 'script' is expected to produce exactly these files under $out.
|
||||||
|
'';
|
||||||
|
defaultText = "attrs of files";
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (file: {
|
||||||
|
imports = [
|
||||||
|
config.vars.settings.fileModule
|
||||||
|
];
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.strMatching "[a-zA-Z0-9:_\\.-]*";
|
||||||
|
description = ''
|
||||||
|
name of the generated file
|
||||||
|
'';
|
||||||
|
readOnly = true;
|
||||||
|
default = file.config._module.args.name;
|
||||||
|
defaultText = "Name of the file";
|
||||||
|
};
|
||||||
|
generator = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The generator that produces the file.
|
||||||
|
This is the name of another generator.
|
||||||
|
'';
|
||||||
|
type = lib.types.strMatching "[a-zA-Z0-9:_\\.-]*";
|
||||||
|
readOnly = true;
|
||||||
|
internal = true;
|
||||||
|
default = generator.config.name;
|
||||||
|
defaultText = "Name of the generator";
|
||||||
|
};
|
||||||
|
deploy = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Whether the file should be deployed to the target machine.
|
||||||
|
|
||||||
|
Disable this if the generated file is only used as an input to other generators.
|
||||||
|
'';
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
secret = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
Whether the file should be treated as a secret.
|
||||||
|
'';
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
path = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The path to the file containing the content of the generated value.
|
||||||
|
This will be set automatically
|
||||||
|
'';
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
prompts = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
A set of prompts to ask the user for values.
|
||||||
|
Prompts are available to the generator script as files.
|
||||||
|
For example, a prompt named 'prompt1' will be available via $prompts/prompt1
|
||||||
|
'';
|
||||||
|
default = { };
|
||||||
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.submodule (prompt: {
|
||||||
|
options = {
|
||||||
|
name = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The name of the prompt.
|
||||||
|
This name will be used to refer to the prompt in the generator script.
|
||||||
|
'';
|
||||||
|
type = lib.types.strMatching "[a-zA-Z0-9:_\\.-]*";
|
||||||
|
default = prompt.config._module.args.name;
|
||||||
|
defaultText = "Name of the prompt";
|
||||||
|
};
|
||||||
|
description = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The description of the prompted value
|
||||||
|
'';
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "SSH private key";
|
||||||
|
default = prompt.config._module.args.name;
|
||||||
|
defaultText = "Name of the prompt";
|
||||||
|
};
|
||||||
|
type = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The input type of the prompt.
|
||||||
|
The following types are available:
|
||||||
|
- hidden: A hidden text (e.g. password)
|
||||||
|
- line: A single line of text
|
||||||
|
- multiline: A multiline text
|
||||||
|
'';
|
||||||
|
type = lib.types.enum [
|
||||||
|
"hidden"
|
||||||
|
"line"
|
||||||
|
"multiline"
|
||||||
|
];
|
||||||
|
default = "line";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
runtimeInputs = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
A list of packages that the generator script requires.
|
||||||
|
These packages will be available in the PATH when the script is run.
|
||||||
|
'';
|
||||||
|
type = lib.types.listOf lib.types.package;
|
||||||
|
default = [ pkgs.coreutils ];
|
||||||
|
};
|
||||||
|
script = lib.mkOption {
|
||||||
|
description = ''
|
||||||
|
The script to run to generate the files.
|
||||||
|
The script will be run with the following environment variables:
|
||||||
|
- $in: The directory containing the output values of all declared dependencies
|
||||||
|
- $out: The output directory to put the generated files
|
||||||
|
- $prompts: The directory containing the prompted values as files
|
||||||
|
The script should produce the files specified in the 'files' attribute under $out.
|
||||||
|
'';
|
||||||
|
type = lib.types.either lib.types.str lib.types.path;
|
||||||
|
default = "";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
117
testing.nix
Normal file
117
testing.nix
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
{ lib, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
name = "vars";
|
||||||
|
meta.maintainers = with lib.maintainers; [ lassulus ];
|
||||||
|
|
||||||
|
nodes.machine =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./options.nix
|
||||||
|
./backends/on-machine.nix
|
||||||
|
];
|
||||||
|
vars.settings.on-machine.enable = true;
|
||||||
|
vars.generators = {
|
||||||
|
simple = {
|
||||||
|
files.simple = { };
|
||||||
|
script = ''
|
||||||
|
echo simple > "$out"/simple
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
a = {
|
||||||
|
files.a = { };
|
||||||
|
script = ''
|
||||||
|
echo a > "$out"/a
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
b = {
|
||||||
|
dependencies = [ "a" ];
|
||||||
|
files.b = { };
|
||||||
|
script = ''
|
||||||
|
cat "$in"/a/a > "$out"/b
|
||||||
|
echo b >> "$out"/b
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
prompts = {
|
||||||
|
files.prompt_line = { };
|
||||||
|
files.prompt_hidden = { };
|
||||||
|
files.prompt_multiline = { };
|
||||||
|
prompts.line = {
|
||||||
|
description = ''
|
||||||
|
a simple line prompt
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
prompts.hidden = {
|
||||||
|
type = "hidden";
|
||||||
|
description = ''
|
||||||
|
a prompt that doesn't show the input
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
prompts.aamulti = {
|
||||||
|
type = "multiline";
|
||||||
|
description = ''
|
||||||
|
a prompt with multiple lines
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
cp "$prompts"/line "$out"/prompt_line
|
||||||
|
cp "$prompts"/hidden "$out"/prompt_hidden
|
||||||
|
cp "$prompts"/aamulti "$out"/prompt_multiline
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
{ nodes, ... }:
|
||||||
|
''
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["${nodes.machine.config.system.build.generate-vars}/bin/generate-vars"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
env={
|
||||||
|
"OUT_DIR": "./vars",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Function to check for expected outputs and send corresponding texts
|
||||||
|
|
||||||
|
def interact_with_process(process, interactions):
|
||||||
|
while interactions:
|
||||||
|
output = process.stdout.readline()
|
||||||
|
if output:
|
||||||
|
print(output.strip()) # Print the output for debugging
|
||||||
|
for expected_output, text_to_send in interactions:
|
||||||
|
if expected_output in output:
|
||||||
|
print("sending", text_to_send)
|
||||||
|
process.stdin.write(text_to_send + '\n')
|
||||||
|
process.stdin.flush()
|
||||||
|
interactions.remove((expected_output, text_to_send))
|
||||||
|
break
|
||||||
|
|
||||||
|
interactions = [
|
||||||
|
("a simple line prompt", "simple prompt content"),
|
||||||
|
("a prompt that doesn't show the input", "hidden prompt content"),
|
||||||
|
("press control-d to finish", f"multi line content1\nmulti line content2\n\n{chr(4)}\n"),
|
||||||
|
("another prompt after EOF", "another prompt content"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Interact with the process
|
||||||
|
interact_with_process(process, interactions)
|
||||||
|
|
||||||
|
# Wait for the process to complete
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
vars_folder = Path("vars")
|
||||||
|
print(list(vars_folder.glob("*")))
|
||||||
|
assert((vars_folder / "secret" / "a" / "a").exists())
|
||||||
|
|
||||||
|
'';
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue