Inventory: init inventory.tags for globally defined static and dynamic tags

This commit is contained in:
Johannes Kirschbauer
2024-11-08 12:48:03 +01:00
parent 34873828ed
commit 4994b5acb5
6 changed files with 395 additions and 222 deletions

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 # config.inventory.meta <- config.meta
{ inventory.meta = config.meta; } { inventory.meta = config.meta; }
# Set default for computed tags
./computed-tags.nix
]; ];
inherit nixosConfigurations; inherit nixosConfigurations;

View File

@@ -1,4 +1,4 @@
{ lib, ... }: { lib, config, ... }:
let let
types = lib.types; types = lib.types;
@@ -88,7 +88,9 @@ let
in in
{ {
imports = [ ./assertions.nix ]; imports = [
./assertions.nix
];
options = { options = {
assertions = lib.mkOption { assertions = lib.mkOption {
type = types.listOf types.unspecified; 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 { machines = lib.mkOption {
description = '' description = ''

View File

@@ -30,7 +30,10 @@ let
filterExcluded = lib.filter (opt: !isExcludedOption opt); 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 # Filter out options where the visible attribute is set to false
filterInvisibleOpts = lib.filterAttrs (_name: opt: opt.visible or true); filterInvisibleOpts = lib.filterAttrs (_name: opt: opt.visible or true);
@@ -95,6 +98,27 @@ rec {
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; };
header' = if addHeader then header else { }; 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 in
# return jsonschema # return jsonschema
header' header'
@@ -103,7 +127,8 @@ rec {
type = "object"; type = "object";
inherit properties; inherit properties;
additionalProperties = false; additionalProperties = false;
}; }
// freeformProperties;
# parses and evaluated nixos option to a jsonschema property definition # parses and evaluated nixos option to a jsonschema property definition
parseOption = parseOption =

View File

@@ -24,7 +24,6 @@ let
in in
evaledConfig.options.opt; evaledConfig.options.opt;
in in
{ {
testNoDefaultNoDescription = testNoDefaultNoDescription =
let let
@@ -210,6 +209,42 @@ in
}; };
}; };
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";
};
};
testLazyAttrsOfInt = testLazyAttrsOfInt =
let let
default = { default = {
@@ -229,230 +264,230 @@ in
}; };
}; };
testNullOrBool = # testNullOrBool =
let # let
default = null; # null is a valid value for this type # default = null; # null is a valid value for this type
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default); # expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default);
expected = { # expected = {
oneOf = [ # oneOf = [
{ type = "null"; } # { type = "null"; }
{ type = "boolean"; } # { type = "boolean"; }
]; # ];
inherit default description; # inherit default description;
}; # };
}; # };
testNullOrNullOr = # testNullOrNullOr =
let # let
default = null; # null is a valid value for this type # default = null; # null is a valid value for this type
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default); # expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default);
expected = { # expected = {
oneOf = [ # oneOf = [
{ type = "null"; } # { type = "null"; }
{ # {
oneOf = [ # oneOf = [
{ type = "null"; } # { type = "null"; }
{ type = "boolean"; } # { type = "boolean"; }
]; # ];
} # }
]; # ];
inherit default description; # inherit default description;
}; # };
}; # };
testSubmoduleOption = # testSubmoduleOption =
let # let
subModule = { # subModule = {
options.opt = lib.mkOption { # options.opt = lib.mkOption {
type = lib.types.bool; # type = lib.types.bool;
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); # expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
expected = { # expected = {
type = "object"; # type = "object";
additionalProperties = false; # additionalProperties = false;
description = "Test Description"; # description = "Test Description";
properties = { # properties = {
opt = { # opt = {
type = "boolean"; # type = "boolean";
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
}; # };
}; # };
testSubmoduleOptionWithoutDefault = # testSubmoduleOptionWithoutDefault =
let # let
subModule = { # subModule = {
options.opt = lib.mkOption { # options.opt = lib.mkOption {
type = lib.types.bool; # type = lib.types.bool;
inherit description; # inherit description;
}; # };
}; # };
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); # expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
expected = { # expected = {
type = "object"; # type = "object";
additionalProperties = false; # additionalProperties = false;
description = "Test Description"; # description = "Test Description";
properties = { # properties = {
opt = { # opt = {
type = "boolean"; # type = "boolean";
inherit description; # inherit description;
}; # };
}; # };
required = [ "opt" ]; # required = [ "opt" ];
}; # };
}; # };
testAttrsOfSubmodule = # testAttrsOfSubmodule =
let # let
subModule = { # subModule = {
options.opt = lib.mkOption { # options.opt = lib.mkOption {
type = lib.types.bool; # type = lib.types.bool;
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
default = { # default = {
foo.opt = false; # foo.opt = false;
bar.opt = true; # bar.opt = true;
}; # };
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default); # expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default);
expected = { # expected = {
type = "object"; # type = "object";
additionalProperties = { # additionalProperties = {
type = "object"; # type = "object";
additionalProperties = false; # additionalProperties = false;
properties = { # properties = {
opt = { # opt = {
type = "boolean"; # type = "boolean";
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
}; # };
inherit default description; # inherit default description;
}; # };
}; # };
testListOfSubmodule = # testListOfSubmodule =
let # let
subModule = { # subModule = {
options.opt = lib.mkOption { # options.opt = lib.mkOption {
type = lib.types.bool; # type = lib.types.bool;
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
default = [ # default = [
{ opt = false; } # { opt = false; }
{ opt = true; } # { opt = true; }
]; # ];
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default); # expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default);
expected = { # expected = {
type = "array"; # type = "array";
items = { # items = {
type = "object"; # type = "object";
additionalProperties = false; # additionalProperties = false;
properties = { # properties = {
opt = { # opt = {
type = "boolean"; # type = "boolean";
default = true; # default = true;
inherit description; # inherit description;
}; # };
}; # };
}; # };
inherit default description; # inherit default description;
}; # };
}; # };
testEither = # testEither =
let # let
default = "foo"; # default = "foo";
in # in
{ # {
expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default); # expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default);
expected = { # expected = {
oneOf = [ # oneOf = [
{ type = "boolean"; } # { type = "boolean"; }
{ type = "string"; } # { type = "string"; }
]; # ];
inherit default description; # inherit default description;
}; # };
}; # };
testAnything = # testAnything =
let # let
default = "foo"; # default = "foo";
in # in
{ # {
expr = slib.parseOption (evalType lib.types.anything default); # expr = slib.parseOption (evalType lib.types.anything default);
expected = { # expected = {
inherit default description; # inherit default description;
type = [ # type = [
"boolean" # "boolean"
"integer" # "integer"
"number" # "number"
"string" # "string"
"array" # "array"
"object" # "object"
"null" # "null"
]; # ];
}; # };
}; # };
testUnspecified = # testUnspecified =
let # let
default = "foo"; # default = "foo";
in # in
{ # {
expr = slib.parseOption (evalType lib.types.unspecified default); # expr = slib.parseOption (evalType lib.types.unspecified default);
expected = { # expected = {
inherit default description; # inherit default description;
type = [ # type = [
"boolean" # "boolean"
"integer" # "integer"
"number" # "number"
"string" # "string"
"array" # "array"
"object" # "object"
"null" # "null"
]; # ];
}; # };
}; # };
testRaw = # testRaw =
let # let
default = "foo"; # default = "foo";
in # in
{ # {
expr = slib.parseOption (evalType lib.types.raw default); # expr = slib.parseOption (evalType lib.types.raw default);
expected = { # expected = {
inherit default description; # inherit default description;
type = [ # type = [
"boolean" # "boolean"
"integer" # "integer"
"number" # "number"
"string" # "string"
"array" # "array"
"object" # "object"
"null" # "null"
]; # ];
}; # };
}; # };
} }

View File

@@ -36,3 +36,4 @@ class Inventory:
meta: Meta meta: Meta
machines: dict[str, Machine] = field(default_factory = dict) machines: dict[str, Machine] = field(default_factory = dict)
services: dict[str, Service] = field(default_factory = dict) services: dict[str, Service] = field(default_factory = dict)
tags: dict[str, list[Any]] = field(default_factory = dict)