Inventory/schemas: use less schema versions

This commit is contained in:
Johannes Kirschbauer
2024-10-09 11:30:22 +02:00
parent 9e8c7ffcce
commit 0a5223a1f0
7 changed files with 24 additions and 322 deletions

View File

@@ -1,6 +0,0 @@
# shellcheck shell=bash
source_up
watch_file flake-module.nix
use flake .#inventory-schema --builders ''

View File

@@ -1,53 +0,0 @@
{
"machines": {
"camina_machine": {
"name": "camina",
"tags": ["laptop"]
},
"vyr_machine": {
"name": "vyr"
},
"vi_machine": {
"name": "vi",
"tags": ["laptop"]
}
},
"meta": {
"name": "kenjis clan"
},
"services": {
"borgbackup": {
"instance_1": {
"meta": {
"name": "My backup"
},
"roles": {
"server": {
"machines": ["vyr_machine"]
},
"client": {
"machines": ["vyr_machine"],
"tags": ["laptop"]
}
},
"machines": {},
"config": {}
},
"instance_2": {
"meta": {
"name": "My backup"
},
"roles": {
"server": {
"machines": ["vi_machine"]
},
"client": {
"machines": ["camina_machine"]
}
},
"machines": {},
"config": {}
}
}
}
}

View File

@@ -16,22 +16,7 @@ in
self', self',
... ...
}: }:
let
getSchema = import ./interface-to-schema.nix { inherit lib self; };
# The schema for the inventory, without default values, from the module system.
# This is better suited for human reading and for generating code.
bareSchema = getSchema { includeDefaults = false; };
# The schema for the inventory with default values, from the module system.
# This is better suited for validation, since default values are included.
fullSchema = getSchema { };
in
{ {
legacyPackages.inventory = {
inherit fullSchema;
inherit bareSchema;
};
devShells.inventory-schema = pkgs.mkShell { devShells.inventory-schema = pkgs.mkShell {
inputsFrom = with config.checks; [ inputsFrom = with config.checks; [
lib-inventory-examples-cue lib-inventory-examples-cue
@@ -40,49 +25,12 @@ in
]; ];
}; };
# Inventory schema with concrete module implementations
packages.inventory-api-docs = pkgs.stdenv.mkDerivation {
name = "inventory-schema";
buildInputs = [ ];
src = ./.;
buildPhase = ''
cat <<EOF > "$out"
# Inventory API
*Inventory* is an abstract service layer for consistently configuring distributed services across machine boundaries.
The following is a specification of the inventory in [cuelang](https://cuelang.org/) format.
\`\`\`cue
EOF
cat ${self'.packages.inventory-schema-pretty}/schema.cue >> $out
cat <<EOF >> $out
\`\`\`
EOF
'';
};
packages.inventory-schema = pkgs.stdenv.mkDerivation {
name = "inventory-schema";
buildInputs = [ pkgs.cue ];
src = ./.;
buildPhase = ''
export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON fullSchema.schemaWithModules)}
cp $SCHEMA schema.json
cue import -f -p compose -l '#Root:' schema.json
mkdir $out
cp schema.cue $out
cp schema.json $out
'';
};
packages.inventory-schema-abstract = pkgs.stdenv.mkDerivation { packages.inventory-schema-abstract = pkgs.stdenv.mkDerivation {
name = "inventory-schema"; name = "inventory-schema";
buildInputs = [ pkgs.cue ]; buildInputs = [ pkgs.cue ];
src = ./.; src = ./.;
buildPhase = '' buildPhase = ''
export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON bareSchema.abstractSchema)} export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON self'.legacyPackages.schemas.inventory)}
cp $SCHEMA schema.json cp $SCHEMA schema.json
cue import -f -p compose -l '#Root:' schema.json cue import -f -p compose -l '#Root:' schema.json
mkdir $out mkdir $out
@@ -102,20 +50,6 @@ in
} }
); );
packages.inventory-schema-pretty = pkgs.stdenv.mkDerivation {
name = "inventory-schema-pretty";
buildInputs = [ pkgs.cue ];
src = ./.;
buildPhase = ''
export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON bareSchema.schemaWithModules)}
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 # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
legacyPackages.evalTests-inventory = import ./tests { legacyPackages.evalTests-inventory = import ./tests {
inherit lib; inherit lib;
@@ -123,35 +57,6 @@ in
}; };
checks = { checks = {
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
cp ${self'.packages.inventory-schema}/schema.cue root.cue
ls -la .
echo "Validate test/*.json against inventory-schema..."
cat root.cue
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
'';
};
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)" export HOME="$(realpath .)"

View File

@@ -1,135 +0,0 @@
{ lib, self, ... }:
{
includeDefaults ? true,
}:
let
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 { inherit includeDefaults; };
jsonLib' = self.lib.jsonschema {
inherit includeDefaults;
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;
# The actual schema for the inventory
# !!! We cannot import the module into the interface.nix, because it would cause evaluation overhead.
# Modifies:
# - service.<serviceName>.<instanceName>.config = moduleSchema
# - service.<serviceName>.<instanceName>.machine.<machineName>.config = moduleSchema
# - service.<serviceName>.<instanceName>.roles = acutalRoles
schema =
let
moduleToService = moduleName: moduleSchema: {
type = "object";
additionalProperties = {
type = "object";
additionalProperties = false;
properties = {
meta = {
title = "service-meta";
} // inventorySchema.properties.services.additionalProperties.additionalProperties.properties.meta;
config = {
title = "${moduleName}-config";
default = { };
} // moduleSchema;
roles = {
type = "object";
additionalProperties = false;
required = [ ];
properties = lib.listToAttrs (
map (role: {
name = role;
value =
lib.recursiveUpdate
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.roles.additionalProperties
{
properties.config = {
title = "${moduleName}-config";
default = { };
} // moduleSchema;
};
}) (rolesOf moduleName)
);
};
machines =
lib.recursiveUpdate
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.machines
{
additionalProperties.properties.config = {
title = "${moduleName}-config";
default = { };
} // moduleSchema;
};
};
};
};
rolesOf =
moduleName:
let
# null | [ string ]
roles = getRoles self.clanModules.${moduleName};
in
if roles == null then [ ] else roles;
moduleServices = lib.mapAttrs moduleToService (
lib.filterAttrs (n: _v: rolesOf n != [ ]) modulesSchema
);
in
inventorySchema
// {
properties = inventorySchema.properties // {
services = {
type = "object";
additionalProperties = false;
properties = moduleServices;
};
};
};
in
{
/*
The abstract inventory without the exact schema for each module filled
InventorySchema<T extends Any> :: {
serviceConfig :: dict[str, T];
}
*/
abstractSchema = inventorySchema;
/*
The inventory with each module schema filled.
InventorySchema<T extends ModuleSchema> :: {
${serviceConfig} :: T; # <- each concrete module name is filled
}
*/
schemaWithModules = schema;
inherit modulesSchema;
}

View File

@@ -23,6 +23,7 @@ let
}; };
inventorySchema = jsonLib.parseModule (import ../build-inventory/interface.nix); inventorySchema = jsonLib.parseModule (import ../build-inventory/interface.nix);
renderSchema = pkgs.writers.writePython3Bin "render-schema" { renderSchema = pkgs.writers.writePython3Bin "render-schema" {
flakeIgnore = [ flakeIgnore = [
"F401" "F401"

View File

@@ -36,13 +36,19 @@ def service_roles_to_schema(
schema: dict[str, Any], schema: dict[str, Any],
service_name: str, service_name: str,
roles: list[str], roles: list[str],
roles_schemas: list[dict[str, Any]], roles_schemas: dict[str, dict[str, Any]],
# Original service properties: {'config': Schema, 'machines': Schema, 'meta': Schema, 'extraModules': Schema, ...?} # Original service properties: {'config': Schema, 'machines': Schema, 'meta': Schema, 'extraModules': Schema, ...?}
orig: dict[str, Any], orig: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Add roles to the service schema Add roles to the service schema
""" """
# collect all the roles for the service, to form a type union
all_roles_schema: list[dict[str, Any]] = []
for role_name, role_schema in roles_schemas.items():
role_schema["title"] = f"{module_name}-config-role-{role_name}"
all_roles_schema.append(role_schema)
role_schema = {} role_schema = {}
for role in roles: for role in roles:
role_schema[role] = { role_schema[role] = {
@@ -51,8 +57,8 @@ def service_roles_to_schema(
"properties": { "properties": {
**orig["roles"]["additionalProperties"]["properties"], **orig["roles"]["additionalProperties"]["properties"],
"config": { "config": {
**roles_schemas.get(role, {}),
"title": f"{service_name}-config-role-{role}", "title": f"{service_name}-config-role-{role}",
"oneOf": roles_schemas,
"type": "object", "type": "object",
"default": {}, "default": {},
"additionalProperties": False, "additionalProperties": False,
@@ -68,7 +74,7 @@ def service_roles_to_schema(
**orig["machines"]["additionalProperties"]["properties"], **orig["machines"]["additionalProperties"]["properties"],
"config": { "config": {
"title": f"{service_name}-config", "title": f"{service_name}-config",
"oneOf": roles_schemas, "oneOf": all_roles_schema,
"type": "object", "type": "object",
"default": {}, "default": {},
"additionalProperties": False, "additionalProperties": False,
@@ -95,7 +101,7 @@ def service_roles_to_schema(
"machines": machines_schema, "machines": machines_schema,
"config": { "config": {
"title": f"{service_name}-config", "title": f"{service_name}-config",
"oneOf": roles_schemas, "oneOf": all_roles_schema,
"type": "object", "type": "object",
"default": {}, "default": {},
"additionalProperties": False, "additionalProperties": False,
@@ -134,17 +140,10 @@ if __name__ == "__main__":
"additionalProperties": False, "additionalProperties": False,
} }
for module_name, roles_schema in modules_schema.items(): for module_name, roles_schemas in modules_schema.items():
# collect all the roles for the service
roles_schemas = []
for role_name, role_schema in roles_schema.items():
role_schema["title"] = f"{module_name}-config-role-{role_name}"
roles_schemas.append(role_schema)
# Add the roles schemas to the service schema # Add the roles schemas to the service schema
if roles_schemas: roles = list(roles_schemas.keys())
roles = list(roles_schema.keys()) if roles:
services = service_roles_to_schema( services = service_roles_to_schema(
services, services,
module_name, module_name,

View File

@@ -71,7 +71,7 @@
]; ];
installPhase = '' installPhase = ''
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service" ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema-abstract}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service"
python docs.py reference python docs.py reference
mkdir -p $out mkdir -p $out
@@ -85,30 +85,21 @@
buildInputs = [ buildInputs = [
pkgs.python3 pkgs.python3
self'.packages.json2ts pkgs.json2ts
# TODO: see postFixup clan-cli/default.nix:L188
self'.packages.clan-cli.propagatedBuildInputs
]; ];
installPhase = '' installPhase = ''
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service" ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema-abstract}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service"
mkdir -p $out mkdir -p $out
# Retrieve python API Typescript types
python api.py > $out/API.json python api.py > $out/API.json
${self'.packages.json2ts}/bin/json2ts --input $out/API.json > $out/API.ts json2ts --input $out/API.json > $out/API.ts
${self'.packages.json2ts}/bin/json2ts --input ${self'.packages.inventory-schema}/schema.json > $out/Inventory.ts
cp ${self'.packages.inventory-schema}/schema.json $out/inventory-schema.json # Retrieve python API Typescript types
json2ts --input ${self'.legacyPackages.schemas.inventory}/schema.json > $out/Inventory.ts
cp ${self'.legacyPackages.schemas.inventory}/* $out
''; '';
}; };
json2ts = pkgs.buildNpmPackage {
name = "json2ts";
src = pkgs.fetchFromGitHub {
owner = "bcherny";
repo = "json-schema-to-typescript";
rev = "118d6a8e7a5a9397d1d390ce297f127ae674a623";
hash = "sha256-ldAFfw3E0A0lIJyDSsshgPRPR7OmV/FncPsDhC3waT8=";
};
npmDepsHash = "sha256-kLKau4SBxI9bMAd7X8/FQfCza2sYl/+0bg2LQcOQIJo=";
};
default = self'.packages.clan-cli; default = self'.packages.clan-cli;
}; };