From 3777a4cf0252a99a26d36fcaba69233733392e16 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 26 Jun 2024 15:10:21 +0200 Subject: [PATCH 1/3] Add toml frontmatter description to jsonschema --- lib/default.nix | 3 +- lib/description.nix | 61 ++++++++++++++++++----------------- pkgs/schemas/flake-module.nix | 3 +- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/lib/default.nix b/lib/default.nix index 30b68b5c..086fcce8 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -8,7 +8,6 @@ evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; }; inventory = import ./inventory { inherit lib clan-core; }; jsonschema = import ./jsonschema { inherit lib; }; - # TODO: migrate to also use toml frontmatter - # modules = import ./description.nix { inherit clan-core lib; }; + modules = import ./description.nix { inherit clan-core lib; }; buildClan = import ./build-clan { inherit clan-core lib nixpkgs; }; } diff --git a/lib/description.nix b/lib/description.nix index 917836f0..c4c641e6 100644 --- a/lib/description.nix +++ b/lib/description.nix @@ -1,33 +1,34 @@ -{ ... }: - +{ clan-core, lib }: rec { - # getReadme = - # modulename: - # let - # readme = "${clan-core}/clanModules/${modulename}/README.md"; - # readmeContents = - # if (builtins.pathExists readme) then - # (builtins.readFile readme) - # else - # throw "No README.md found for module ${modulename}"; - # in - # readmeContents; + getReadme = + modulename: + let + readme = "${clan-core}/clanModules/${modulename}/README.md"; + readmeContents = + if (builtins.pathExists readme) then + (builtins.readFile readme) + else + throw "No README.md found for module ${modulename}"; + in + readmeContents; - # getShortDescription = - # modulename: - # let - # content = (getReadme modulename); - # parts = lib.splitString "---" content; - # description = builtins.head parts; - # number_of_newlines = builtins.length (lib.splitString "\n" description); - # in - # if (builtins.length parts) > 1 then - # if number_of_newlines > 4 then - # throw "Short description in README.md for module ${modulename} is too long. Max 3 newlines." - # else if number_of_newlines <= 1 then - # throw "Missing short description in README.md for module ${modulename}." - # else - # description - # else - # throw "Short description delimiter `---` not found in README.md for module ${modulename}"; + getShortDescription = + modulename: + let + content = getReadme modulename; + parts = lib.splitString "---" content; + # Partition the parts into the first part (the readme content) and the rest (the metadata) + parsed = builtins.partition ({ index }: if index >= 2 then false else true) ( + lib.filter ({ index, ... }: index != 0) (lib.imap0 (index: part: { inherit index part; }) parts) + ); + + # Use this if the content is needed + # readmeContent = lib.concatMapStrings (v: "---" + v.part) parsed.wrong; + + meta = builtins.fromTOML (builtins.head parsed.right).part; + in + if (builtins.length parts >= 3) then + meta.description + else + throw "Short description delimiter `---` not found in README.md for module ${modulename}"; } diff --git a/pkgs/schemas/flake-module.nix b/pkgs/schemas/flake-module.nix index 89d75577..59c25d01 100644 --- a/pkgs/schemas/flake-module.nix +++ b/pkgs/schemas/flake-module.nix @@ -23,8 +23,7 @@ clanModuleFunctionSchemas = lib.mapAttrsFlatten (modulename: _: { name = modulename; - # TODO: migrate to new toml format - # description = self.lib.modules.getShortDescription modulename; + description = self.lib.modules.getShortDescription modulename; parameters = self.lib.jsonschema.parseOptions (optionsFromModule modulename); }) clanModules; in From 2535fdcb1254c660e47a9d0f2da11fb539ca181a Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 26 Jun 2024 15:16:20 +0200 Subject: [PATCH 2/3] Inventory: restructure folders --- lib/description.nix | 2 +- lib/inventory/example.nix | 1 + .../{src/tests => examples}/borgbackup.json | 0 .../{src/tests => examples}/syncthing.json | 0 .../{src/tests => examples}/zerotier.json | 0 lib/inventory/flake-module.nix | 23 ++++++++----------- .../{src => spec}/cue.mod/module.cue | 0 lib/inventory/{src => spec}/root.cue | 0 lib/inventory/{src => spec}/schema/schema.cue | 0 9 files changed, 11 insertions(+), 15 deletions(-) rename lib/inventory/{src/tests => examples}/borgbackup.json (100%) rename lib/inventory/{src/tests => examples}/syncthing.json (100%) rename lib/inventory/{src/tests => examples}/zerotier.json (100%) rename lib/inventory/{src => spec}/cue.mod/module.cue (100%) rename lib/inventory/{src => spec}/root.cue (100%) rename lib/inventory/{src => spec}/schema/schema.cue (100%) diff --git a/lib/description.nix b/lib/description.nix index c4c641e6..4e4ad081 100644 --- a/lib/description.nix +++ b/lib/description.nix @@ -18,7 +18,7 @@ rec { content = getReadme modulename; parts = lib.splitString "---" content; # Partition the parts into the first part (the readme content) and the rest (the metadata) - parsed = builtins.partition ({ index }: if index >= 2 then false else true) ( + parsed = builtins.partition ({ index, ... }: if index >= 2 then false else true) ( lib.filter ({ index, ... }: index != 0) (lib.imap0 (index: part: { inherit index part; }) parts) ); diff --git a/lib/inventory/example.nix b/lib/inventory/example.nix index 50b41792..f7a7154e 100644 --- a/lib/inventory/example.nix +++ b/lib/inventory/example.nix @@ -19,6 +19,7 @@ self.lib.buildClan { machines = { "backup_server" = { clan.tags = [ "all" ]; + # ... rest of the machine config }; "client_1_machine" = { clan.tags = [ diff --git a/lib/inventory/src/tests/borgbackup.json b/lib/inventory/examples/borgbackup.json similarity index 100% rename from lib/inventory/src/tests/borgbackup.json rename to lib/inventory/examples/borgbackup.json diff --git a/lib/inventory/src/tests/syncthing.json b/lib/inventory/examples/syncthing.json similarity index 100% rename from lib/inventory/src/tests/syncthing.json rename to lib/inventory/examples/syncthing.json diff --git a/lib/inventory/src/tests/zerotier.json b/lib/inventory/examples/zerotier.json similarity index 100% rename from lib/inventory/src/tests/zerotier.json rename to lib/inventory/examples/zerotier.json diff --git a/lib/inventory/flake-module.nix b/lib/inventory/flake-module.nix index 8778e465..f2d7ed09 100644 --- a/lib/inventory/flake-module.nix +++ b/lib/inventory/flake-module.nix @@ -1,34 +1,29 @@ -{ ... }: +{ self, ... }: { + + flake.inventory = import ./example.nix { inherit self; }; perSystem = { pkgs, config, ... }: { - packages.inventory-schema = pkgs.stdenv.mkDerivation { - name = "inventory-schema"; - src = ./src; - - buildInputs = [ pkgs.cue ]; - - installPhase = '' - mkdir -p $out - ''; + devShells.inventory-schema = pkgs.mkShell { + inputsFrom = [ config.checks.inventory-schema-checks ]; }; - devShells.inventory-schema = pkgs.mkShell { inputsFrom = [ config.packages.inventory-schema ]; }; - checks.inventory-schema-checks = pkgs.stdenv.mkDerivation { name = "inventory-schema-checks"; - src = ./src; + src = ./.; buildInputs = [ pkgs.cue ]; buildPhase = '' echo "Running inventory tests..." + # Cue is easier to run in the same directory as the schema + cd spec echo "Export cue as json-schema..." cue export --out openapi root.cue echo "Validate test/*.json against inventory-schema..." - test_dir="test" + test_dir="../examples" for file in "$test_dir"/*; do # Check if the item is a file if [ -f "$file" ]; then diff --git a/lib/inventory/src/cue.mod/module.cue b/lib/inventory/spec/cue.mod/module.cue similarity index 100% rename from lib/inventory/src/cue.mod/module.cue rename to lib/inventory/spec/cue.mod/module.cue diff --git a/lib/inventory/src/root.cue b/lib/inventory/spec/root.cue similarity index 100% rename from lib/inventory/src/root.cue rename to lib/inventory/spec/root.cue diff --git a/lib/inventory/src/schema/schema.cue b/lib/inventory/spec/schema/schema.cue similarity index 100% rename from lib/inventory/src/schema/schema.cue rename to lib/inventory/spec/schema/schema.cue From 1628fdeaeecc8fadb7e1e18cc7d898d0a6def5a6 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 26 Jun 2024 17:19:19 +0200 Subject: [PATCH 3/3] Inventory: add eval tests --- lib/inventory/build-inventory/default.nix | 13 +- lib/inventory/flake-module.nix | 93 +++++++++---- lib/inventory/tests/default.nix | 151 ++++++++++++++++++++++ 3 files changed, 226 insertions(+), 31 deletions(-) create mode 100644 lib/inventory/tests/default.nix diff --git a/lib/inventory/build-inventory/default.nix b/lib/inventory/build-inventory/default.nix index a5ab65bc..4145354f 100644 --- a/lib/inventory/build-inventory/default.nix +++ b/lib/inventory/build-inventory/default.nix @@ -13,13 +13,18 @@ let ++ (builtins.foldl' ( acc: tag: let + # For error printing + availableTags = lib.foldlAttrs ( + acc: _: v: + v.tags or [ ] ++ acc + ) [ ] inventory.machines; + tagMembers = builtins.attrNames ( lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines ); in - # throw "Machine tag ${tag} not found. Not machine with: tag ${tagName} not in inventory."; if tagMembers == [ ] then - throw "Machine tag ${tag} not found. Not machine with: tag ${tag} not in inventory." + throw "Tag: '${tag}' not found. Available tags: ${builtins.toJSON (lib.unique availableTags)}" else acc ++ tagMembers ) [ ] members.tags or [ ]); @@ -76,7 +81,7 @@ let if builtins.pathExists path then path else - throw "Role doesnt have a module: ${role}. Path: ${path} not found." + throw "Module doesn't have role: '${role}'. Path: ${path} not found." ) inverseRoles.${machineName} or [ ]; in if isInService then @@ -101,6 +106,6 @@ let acc2 ) [ ] serviceConfigs) ) [ ] inventory.services - ) inventory.machines; + ) inventory.machines or { }; in machines diff --git a/lib/inventory/flake-module.nix b/lib/inventory/flake-module.nix index f2d7ed09..fa78fdba 100644 --- a/lib/inventory/flake-module.nix +++ b/lib/inventory/flake-module.nix @@ -1,42 +1,81 @@ -{ self, ... }: +{ self, inputs, ... }: +let + inputOverrides = builtins.concatStringsSep " " ( + builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs) + ); +in { - flake.inventory = import ./example.nix { inherit self; }; + perSystem = - { pkgs, config, ... }: + { + pkgs, + lib, + config, + system, + ... + }: + let + buildInventory = import ./build-inventory { + clan-core = self; + inherit lib; + }; + in { devShells.inventory-schema = pkgs.mkShell { - inputsFrom = [ config.checks.inventory-schema-checks ]; + inputsFrom = with config.checks; [ + lib-inventory-schema + lib-inventory-eval + ]; }; - checks.inventory-schema-checks = pkgs.stdenv.mkDerivation { - name = "inventory-schema-checks"; - src = ./.; - buildInputs = [ pkgs.cue ]; - buildPhase = '' - echo "Running inventory tests..." - # Cue is easier to run in the same directory as the schema - cd spec + # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests + legacyPackages.evalTests = import ./tests { + inherit buildInventory; + clan-core = self; + }; - echo "Export cue as json-schema..." - cue export --out openapi root.cue + checks = { + lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' + export HOME="$(realpath .)" - echo "Validate test/*.json against inventory-schema..." - - test_dir="../examples" - for file in "$test_dir"/*; do - # Check if the item is a file - if [ -f "$file" ]; then - # Print the filename - echo "Running test on: $file" - - # Run the cue vet command - cue vet "$file" root.cue -d "#Root" - fi - done + nix-unit --eval-store "$HOME" \ + --extra-experimental-features flakes \ + ${inputOverrides} \ + --flake ${self}#legacyPackages.${system}.evalTests touch $out ''; + + lib-inventory-schema = pkgs.stdenv.mkDerivation { + name = "inventory-schema-checks"; + src = ./.; + buildInputs = [ pkgs.cue ]; + buildPhase = '' + echo "Running inventory tests..." + # Cue is easier to run in the same directory as the schema + cd spec + + echo "Export cue as json-schema..." + cue export --out openapi root.cue + + echo "Validate test/*.json against inventory-schema..." + + test_dir="../examples" + for file in "$test_dir"/*; do + # Check if the item is a file + if [ -f "$file" ]; then + # Print the filename + echo "Running test on: $file" + + # Run the cue vet command + cue vet "$file" root.cue -d "#Root" + fi + done + + touch $out + ''; + }; }; }; } diff --git a/lib/inventory/tests/default.nix b/lib/inventory/tests/default.nix new file mode 100644 index 00000000..8c549cea --- /dev/null +++ b/lib/inventory/tests/default.nix @@ -0,0 +1,151 @@ +{ buildInventory, clan-core, ... }: +{ + test_inventory_empty = { + # Empty inventory should return an empty module + expr = buildInventory { }; + expected = { }; + }; + test_inventory_role_imports = + let + configs = buildInventory { + services = { + borgbackup.instance_1 = { + roles.server.machines = [ "backup_server" ]; + roles.client.machines = [ + "client_1_machine" + "client_2_machine" + ]; + }; + }; + machines = { + "backup_server" = { }; + "client_1_machine" = { }; + "client_2_machine" = { }; + }; + }; + in + { + expr = { + server_imports = (builtins.head configs."backup_server").imports; + client_1_imports = (builtins.head configs."client_1_machine").imports; + client_2_imports = (builtins.head configs."client_2_machine").imports; + }; + + expected = { + server_imports = [ + clan-core.clanModules.borgbackup + "${clan-core.clanModules.borgbackup}/roles/server.nix" + ]; + client_1_imports = [ + clan-core.clanModules.borgbackup + "${clan-core.clanModules.borgbackup}/roles/client.nix" + ]; + client_2_imports = [ + clan-core.clanModules.borgbackup + "${clan-core.clanModules.borgbackup}/roles/client.nix" + ]; + }; + }; + test_inventory_tag_resolve = + let + configs = buildInventory { + services = { + borgbackup.instance_1 = { + roles.client.tags = [ "backup" ]; + }; + }; + machines = { + "not_used_machine" = { }; + "client_1_machine" = { + tags = [ "backup" ]; + }; + "client_2_machine" = { + tags = [ "backup" ]; + }; + }; + }; + in + { + expr = { + client_1_machine = builtins.length configs.client_1_machine; + client_2_machine = builtins.length configs.client_2_machine; + not_used_machine = builtins.length configs.not_used_machine; + }; + expected = { + client_1_machine = 2; + client_2_machine = 2; + not_used_machine = 0; + }; + }; + + test_inventory_multiple_roles = + let + configs = buildInventory { + services = { + borgbackup.instance_1 = { + roles.client.machines = [ "machine_1" ]; + roles.server.machines = [ "machine_1" ]; + }; + }; + machines = { + "machine_1" = { }; + }; + }; + in + { + expr = { + machine_1_imports = (builtins.head configs."machine_1").imports; + }; + expected = { + machine_1_imports = [ + clan-core.clanModules.borgbackup + "${clan-core.clanModules.borgbackup}/roles/client.nix" + "${clan-core.clanModules.borgbackup}/roles/server.nix" + ]; + }; + }; + + test_inventory_role_doesnt_exist = + let + configs = buildInventory { + services = { + borgbackup.instance_1 = { + roles.roleXYZ.machines = [ "machine_1" ]; + }; + }; + machines = { + "machine_1" = { }; + }; + }; + in + { + expr = configs; + expectedError = { + type = "ThrownError"; + msg = "Module doesn't have role.*"; + }; + }; + test_inventory_tag_doesnt_exist = + let + configs = buildInventory { + services = { + borgbackup.instance_1 = { + roles.client.machines = [ "machine_1" ]; + roles.client.tags = [ "tagXYZ" ]; + }; + }; + machines = { + "machine_1" = { + tags = [ "tagABC" ]; + }; + }; + }; + in + { + expr = configs; + expectedError = { + type = "ThrownError"; + msg = "Tag: '\\w+' not found"; + }; + }; +}