Merge pull request 'Inventory: init inventory.tags for globally defined static and dynamic tags' (#2328) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-11-08 15:01:07 +00:00
10 changed files with 188 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ in
{
options.clan = lib.mkOption {
default = { };
type = types.submoduleWith {
specialArgs = {
inherit clan-core self;

View File

@@ -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
);
}
)
);
};
};
}

View File

@@ -162,6 +162,8 @@ in
#
# config.inventory.meta <- config.meta
{ inventory.meta = config.meta; }
# Set default for computed tags
./computed-tags.nix
];
inherit nixosConfigurations;

View File

@@ -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 = "[ <All Machines> ]";
description = ''
!!! example "Predefined Tag"
Will be added to all machines
```nix
inventory.machines.machineA.tags = [ "all" ];
```
'';
};
};
}
];
};
};
machines = lib.mkOption {
description = ''

View File

@@ -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 =

View File

@@ -24,7 +24,6 @@ let
in
evaledConfig.options.opt;
in
{
testNoDefaultNoDescription =
let

View File

@@ -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";
};
};
}

View File

@@ -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)

View File

@@ -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
'';
};

View File

@@ -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":