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
	
	 lassulus
						lassulus