diff --git a/flakeModules/clan.nix b/flakeModules/clan.nix index b837d92dd..5b7850e27 100644 --- a/flakeModules/clan.nix +++ b/flakeModules/clan.nix @@ -13,6 +13,7 @@ in { options.clan = lib.mkOption { + default = { }; type = types.submoduleWith { specialArgs = { inherit clan-core self; diff --git a/lib/build-clan/computed-tags.nix b/lib/build-clan/computed-tags.nix new file mode 100644 index 000000000..f3ec6a36d --- /dev/null +++ b/lib/build-clan/computed-tags.nix @@ -0,0 +1,33 @@ +{ + config, + lib, + ... +}: +{ + config.inventory = { + tags = ( + { machines, ... }: + { + # Only compute the default value + # The option MUST be defined in ./build-inventory/interface.nix + all = lib.mkDefault (builtins.attrNames machines); + } + ); + }; + # Add the computed tags to machine tags for displaying them + options.inventory = { + machines = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + # 'name' is the machines attribute-name + { name, ... }: + { + tags = builtins.attrNames ( + lib.filterAttrs (_t: tagMemers: builtins.elem name tagMemers) config.inventory.tags + ); + } + ) + ); + }; + }; +} diff --git a/lib/build-clan/module.nix b/lib/build-clan/module.nix index bf8fbe7b0..079a7762c 100644 --- a/lib/build-clan/module.nix +++ b/lib/build-clan/module.nix @@ -162,6 +162,8 @@ in # # config.inventory.meta <- config.meta { inventory.meta = config.meta; } + # Set default for computed tags + ./computed-tags.nix ]; inherit nixosConfigurations; diff --git a/lib/inventory/build-inventory/interface.nix b/lib/inventory/build-inventory/interface.nix index 55fb875df..4e23ba8b3 100644 --- a/lib/inventory/build-inventory/interface.nix +++ b/lib/inventory/build-inventory/interface.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, config, ... }: let types = lib.types; @@ -88,7 +88,9 @@ let in { - imports = [ ./assertions.nix ]; + imports = [ + ./assertions.nix + ]; options = { assertions = lib.mkOption { type = types.listOf types.unspecified; @@ -103,6 +105,81 @@ in ]; }; }; + tags = lib.mkOption { + default = { }; + description = '' + Tags of the inventory are used to group machines together. + + It is recommended to use [`machine.tags`](#machinestags) to define the tags of the machines. + + This can be used to define custom tags that are either statically set or dynamically computed. + + #### Static Tags + + ???+ example "Static Tag Example" + ```nix + inventory.tags = { + foo = [ "machineA" "machineB" ]; + }; + ``` + + The tag `foo` will always be added to `machineA` and `machineB`. + + #### Dynamic Tags + + It is possible to compute tags based on the machines properties or based on other tags. + + !!! danger + This is a powerfull feature and should be used with caution. + + It is possible to cause infinite recursion by computing tags based on the machines properties or based on other tags. + + ???+ example "Dynamic Tag Example" + + allButFoo is a computed tag. It will be added to all machines except 'foo' + + `all` is a predefined tag. See the docs of [`tags.all`](#tagsall). + + ```nix + # inventory.tags ↓ ↓ inventory.machines + inventory.tags = {config, machines...}: { + # ↓↓↓ The "all" tag + allButFoo = builtins.filter (name: name != "foo") config.all; + }; + ``` + + !!! warning + Do NOT compute `tags` from `machine.tags` this will cause infinite recursion. + ''; + type = types.submoduleWith { + specialArgs = { + inherit (config) machines; + }; + modules = [ + { + freeformType = with lib.types; lazyAttrsOf (listOf str); + # Reserved tags + # Defined as options here to show them in advance + options = { + # 'All machines' tag + all = lib.mkOption { + type = with lib.types; listOf str; + defaultText = "[ ]"; + description = '' + !!! example "Predefined Tag" + + Will be added to all machines + + ```nix + inventory.machines.machineA.tags = [ "all" ]; + ``` + ''; + }; + }; + } + ]; + }; + }; machines = lib.mkOption { description = '' diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 366e8238b..9ee155e4e 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -30,7 +30,10 @@ let filterExcluded = lib.filter (opt: !isExcludedOption opt); - filterExcludedAttrs = lib.filterAttrs (_name: opt: !isExcludedOption opt); + excludedOptionNames = [ "_freeformOptions" ]; + filterExcludedAttrs = lib.filterAttrs ( + name: opt: !isExcludedOption opt && !builtins.elem name excludedOptionNames + ); # Filter out options where the visible attribute is set to false filterInvisibleOpts = lib.filterAttrs (_name: opt: opt.visible or true); @@ -95,6 +98,27 @@ rec { requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; header' = if addHeader then header else { }; + + # freeformType is a special type + freeformDefs = (options._module.freeformType.definitions or [ ]); + checkFreeformDefs = + defs: + if (builtins.length defs) != 1 then + throw "parseOptions: freeformType definitions not supported" + else + defs; + # It seems that freeformType has [ null ] + freeformProperties = + if freeformDefs != [ ] && builtins.head freeformDefs != null then + # freeformType has only one definition + parseOption { + # options._module.freeformType.definitions + type = (builtins.head (checkFreeformDefs freeformDefs)); + _type = "option"; + loc = options._module.freeformType.loc; + } + else + { }; in # return jsonschema header' @@ -103,7 +127,8 @@ rec { type = "object"; inherit properties; additionalProperties = false; - }; + } + // freeformProperties; # parses and evaluated nixos option to a jsonschema property definition parseOption = diff --git a/lib/jsonschema/test_parseOption.nix b/lib/jsonschema/test_parseOption.nix index fc8abc7bc..896d47d89 100644 --- a/lib/jsonschema/test_parseOption.nix +++ b/lib/jsonschema/test_parseOption.nix @@ -24,7 +24,6 @@ let in evaledConfig.options.opt; in - { testNoDefaultNoDescription = let diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index f637999a3..fe5d2f169 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -36,4 +36,41 @@ type = "object"; }; }; + + testFreeFormOfInt = + let + default = { + foo = 1; + bar = 2; + }; + in + { + expr = slib.parseOptions (lib.evalModules { + modules = [ + { + freeformType = with lib.types; attrsOf int; + options = { + enable = lib.mkEnableOption "enable this"; + }; + } + default + ]; + }).options { }; + expected = { + "$schema" = "http://json-schema.org/draft-07/schema#"; + additionalProperties = { + type = "integer"; + }; + properties = { + enable = { + default = false; + description = "Whether to enable enable this."; + examples = [ true ]; + type = "boolean"; + }; + }; + type = "object"; + }; + }; + } diff --git a/pkgs/clan-cli/clan_cli/inventory/classes.py b/pkgs/clan-cli/clan_cli/inventory/classes.py index d723d93a5..296230db8 100644 --- a/pkgs/clan-cli/clan_cli/inventory/classes.py +++ b/pkgs/clan-cli/clan_cli/inventory/classes.py @@ -36,3 +36,4 @@ class Inventory: meta: Meta machines: dict[str, Machine] = field(default_factory = dict) services: dict[str, Service] = field(default_factory = dict) + tags: dict[str, list[str]] = field(default_factory = dict) diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index ccd4cc847..297c4ee8d 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -88,6 +88,7 @@ buildInputs = [ pkgs.python3 pkgs.json2ts + pkgs.jq ]; installPhase = '' @@ -98,7 +99,9 @@ json2ts --input $out/API.json > $out/API.ts # Retrieve python API Typescript types - json2ts --input ${self'.legacyPackages.schemas.inventory}/schema.json > $out/Inventory.ts + # delete the reserved tags from typechecking because the conversion library doesn't support them + jq 'del(.properties.tags.properties)' ${self'.legacyPackages.schemas.inventory}/schema.json > schema.json + json2ts --input schema.json > $out/Inventory.ts cp ${self'.legacyPackages.schemas.inventory}/* $out ''; }; diff --git a/pkgs/classgen/main.py b/pkgs/classgen/main.py index be0000bc0..92c56a65e 100644 --- a/pkgs/classgen/main.py +++ b/pkgs/classgen/main.py @@ -24,7 +24,10 @@ def map_json_type( res |= map_json_type(t) return res if isinstance(json_type, dict): - return map_json_type(json_type.get("type")) + items = json_type.get("items") + if items: + nested_types = map_json_type(items) + return map_json_type(json_type.get("type"), nested_types) if json_type == "string": return {"str"} if json_type == "integer":