From 1628fdeaeecc8fadb7e1e18cc7d898d0a6def5a6 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 26 Jun 2024 17:19:19 +0200 Subject: [PATCH] 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"; + }; + }; +}