Inventory: generate exact schema for validation & documentation
This commit is contained in:
@@ -15,7 +15,7 @@ let
|
||||
};
|
||||
|
||||
machineRef = lib.mkOptionType {
|
||||
name = "machineRef";
|
||||
name = "str";
|
||||
description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]";
|
||||
check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines);
|
||||
merge = lib.mergeEqualOption;
|
||||
@@ -29,20 +29,85 @@ let
|
||||
);
|
||||
|
||||
tagRef = lib.mkOptionType {
|
||||
name = "tagRef";
|
||||
name = "str";
|
||||
description = "Tags :: [${builtins.concatStringsSep " | " allTags}]";
|
||||
check = v: lib.isString v && builtins.elem v allTags;
|
||||
merge = lib.mergeEqualOption;
|
||||
};
|
||||
|
||||
moduleConfig = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf t.anything;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.assertions = lib.mkOption {
|
||||
type = t.listOf t.unspecified;
|
||||
internal = true;
|
||||
default = [ ];
|
||||
options = {
|
||||
assertions = lib.mkOption {
|
||||
type = t.listOf t.unspecified;
|
||||
internal = true;
|
||||
visible = false;
|
||||
default = [ ];
|
||||
};
|
||||
meta = metaOptions;
|
||||
|
||||
machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options = {
|
||||
inherit (metaOptions) name description icon;
|
||||
tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf t.str;
|
||||
};
|
||||
system = lib.mkOption {
|
||||
default = null;
|
||||
type = t.nullOr t.str;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
services = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.attrsOf (
|
||||
t.submodule {
|
||||
options.meta = metaOptions;
|
||||
options.config = moduleConfig;
|
||||
options.machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (t.submodule { options.config = moduleConfig; });
|
||||
};
|
||||
options.roles = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options.machines = lib.mkOption {
|
||||
default = [ ];
|
||||
type = t.listOf machineRef;
|
||||
};
|
||||
options.tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf tagRef;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
# Smoke validation of the inventory
|
||||
config.assertions =
|
||||
let
|
||||
# Inventory assertions
|
||||
# - All referenced machines must exist in the top-level machines
|
||||
serviceAssertions = lib.foldlAttrs (
|
||||
ass1: serviceName: c:
|
||||
ass1
|
||||
@@ -60,8 +125,11 @@ in
|
||||
ass2 ++ assertions
|
||||
) [ ] c
|
||||
) [ ] config.services;
|
||||
|
||||
# Machine assertions
|
||||
# - A machine must define their host system
|
||||
machineAssertions = map (
|
||||
{ name, value }:
|
||||
{ name }:
|
||||
{
|
||||
assertion = true;
|
||||
message = "Machine ${name} should define its host system in the inventory. ()";
|
||||
@@ -69,68 +137,4 @@ in
|
||||
) (lib.attrsToList (lib.filterAttrs (_n: v: v.system or null == null) config.machines));
|
||||
in
|
||||
machineAssertions ++ serviceAssertions;
|
||||
|
||||
options.meta = metaOptions;
|
||||
|
||||
options.machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options = {
|
||||
inherit (metaOptions) name description icon;
|
||||
tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf t.str;
|
||||
};
|
||||
system = lib.mkOption {
|
||||
default = null;
|
||||
type = t.nullOr t.str;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
options.services = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.attrsOf (
|
||||
t.submodule {
|
||||
options.meta = metaOptions;
|
||||
options.config = lib.mkOption {
|
||||
default = { };
|
||||
type = t.anything;
|
||||
};
|
||||
options.machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options.config = lib.mkOption {
|
||||
default = { };
|
||||
type = t.anything;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
options.roles = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options.machines = lib.mkOption {
|
||||
default = [ ];
|
||||
type = t.listOf machineRef;
|
||||
};
|
||||
options.tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf tagRef;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"syncthing": {
|
||||
"instance_1": {
|
||||
"meta": {
|
||||
"name": "My sync"
|
||||
},
|
||||
"roles": {
|
||||
"peer": {
|
||||
"machines": ["vyr_machine", "vi_machine", "camina_machine"]
|
||||
}
|
||||
},
|
||||
"machines": {},
|
||||
"config": {
|
||||
"folders": {
|
||||
"test": {
|
||||
"path": "~/data/docs",
|
||||
"devices": ["camina_machine", "vyr_machine", "vi_machine"]
|
||||
},
|
||||
"videos": {
|
||||
"path": "~/data/videos",
|
||||
"devices": ["camina_machine", "vyr_machine"]
|
||||
},
|
||||
"playlist": {
|
||||
"path": "~/data/playlist",
|
||||
"devices": ["camina_machine", "vi_machine"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"zerotier": {
|
||||
"instance_1": {
|
||||
"meta": {
|
||||
"name": "My Network"
|
||||
},
|
||||
"roles": {
|
||||
"controller": { "machines": ["vyr_machine"] },
|
||||
"moon": { "machines": ["vyr_machine"] },
|
||||
"peer": { "machines": ["vi_machine", "camina_machine"] }
|
||||
},
|
||||
"machines": {
|
||||
"vyr_machine": {
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,113 @@ in
|
||||
clan-core = self;
|
||||
inherit lib;
|
||||
};
|
||||
|
||||
optionsFromModule =
|
||||
mName:
|
||||
let
|
||||
eval = self.lib.evalClanModules [ mName ];
|
||||
in
|
||||
if (eval.options.clan ? "${mName}") then eval.options.clan.${mName} else { };
|
||||
|
||||
modulesSchema = lib.mapAttrs (
|
||||
moduleName: _: jsonLib'.parseOptions (optionsFromModule moduleName) { }
|
||||
) self.clanModules;
|
||||
|
||||
jsonLib = self.lib.jsonschema {
|
||||
# includeDefaults = false;
|
||||
};
|
||||
jsonLib' = self.lib.jsonschema {
|
||||
# includeDefaults = false;
|
||||
header = { };
|
||||
};
|
||||
inventorySchema = jsonLib.parseModule (import ./build-inventory/interface.nix);
|
||||
|
||||
getRoles =
|
||||
modulePath:
|
||||
let
|
||||
rolesDir = "${modulePath}/roles";
|
||||
in
|
||||
if builtins.pathExists rolesDir then
|
||||
lib.pipe rolesDir [
|
||||
builtins.readDir
|
||||
(lib.filterAttrs (_n: v: v == "regular"))
|
||||
lib.attrNames
|
||||
(map (fileName: lib.removeSuffix ".nix" fileName))
|
||||
]
|
||||
else
|
||||
null;
|
||||
|
||||
schema = inventorySchema // {
|
||||
properties = inventorySchema.properties // {
|
||||
services = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = lib.mapAttrs (moduleName: moduleSchema: {
|
||||
type = "object";
|
||||
additionalProperties = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
meta =
|
||||
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.meta;
|
||||
config = moduleSchema;
|
||||
roles = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
required = [ ];
|
||||
properties = lib.listToAttrs (
|
||||
map
|
||||
(role: {
|
||||
name = role;
|
||||
value =
|
||||
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.roles.additionalProperties;
|
||||
})
|
||||
(
|
||||
let
|
||||
roles = getRoles self.clanModules.${moduleName};
|
||||
in
|
||||
if roles == null then [ ] else roles
|
||||
)
|
||||
);
|
||||
};
|
||||
machines =
|
||||
lib.recursiveUpdate
|
||||
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.machines
|
||||
{ additionalProperties.properties.config = moduleSchema; };
|
||||
};
|
||||
};
|
||||
}) modulesSchema;
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
legacyPackages.inventorySchema = schema;
|
||||
|
||||
devShells.inventory-schema = pkgs.mkShell {
|
||||
inputsFrom = with config.checks; [
|
||||
lib-inventory-examples-cue
|
||||
lib-inventory-schema
|
||||
lib-inventory-eval
|
||||
self'.devShells.default
|
||||
];
|
||||
};
|
||||
|
||||
# Inventory schema with concrete module implementations
|
||||
packages.inventory-schema = pkgs.stdenv.mkDerivation {
|
||||
name = "inventory-schema";
|
||||
buildInputs = [ pkgs.cue ];
|
||||
src = ./.;
|
||||
buildPhase = ''
|
||||
export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON self'.legacyPackages.inventorySchema)}
|
||||
cp $SCHEMA schema.json
|
||||
cue import -f -p compose -l '#Root:' schema.json
|
||||
mkdir $out
|
||||
cp schema.cue $out
|
||||
cp schema.json $out
|
||||
'';
|
||||
};
|
||||
|
||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
||||
legacyPackages.evalTests-inventory = import ./tests {
|
||||
inherit buildInventory;
|
||||
@@ -38,32 +135,21 @@ in
|
||||
};
|
||||
|
||||
checks = {
|
||||
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.evalTests-inventory
|
||||
|
||||
touch $out
|
||||
'';
|
||||
|
||||
lib-inventory-schema = pkgs.stdenv.mkDerivation {
|
||||
lib-inventory-examples-cue = 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
|
||||
cp ${self'.packages.inventory-schema}/schema.cue root.cue
|
||||
|
||||
echo "Export cue as json-schema..."
|
||||
cue export --out openapi root.cue
|
||||
ls -la .
|
||||
|
||||
echo "Validate test/*.json against inventory-schema..."
|
||||
cat root.cue
|
||||
|
||||
test_dir="../examples"
|
||||
test_dir="./examples"
|
||||
for file in "$test_dir"/*; do
|
||||
# Check if the item is a file
|
||||
if [ -f "$file" ]; then
|
||||
@@ -78,6 +164,16 @@ in
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.evalTests-inventory
|
||||
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user