Merge pull request 'Doc: write inventory documentation' (#1741) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -52,6 +52,7 @@ nav:
|
|||||||
- Flake-parts: getting-started/flake-parts.md
|
- Flake-parts: getting-started/flake-parts.md
|
||||||
- Concepts:
|
- Concepts:
|
||||||
- Configuration: concepts/configuration.md
|
- Configuration: concepts/configuration.md
|
||||||
|
- Inventory: concepts/inventory.md
|
||||||
- Reference:
|
- Reference:
|
||||||
- Clan Modules:
|
- Clan Modules:
|
||||||
- reference/clanModules/borgbackup-static.md
|
- reference/clanModules/borgbackup-static.md
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ The core function that produces a clan. It returns a set of consistent configura
|
|||||||
`inventory`
|
`inventory`
|
||||||
: Service set for easily configuring distributed services, such as backups
|
: Service set for easily configuring distributed services, such as backups
|
||||||
|
|
||||||
: For more details see [Inventory](#inventory)
|
: For more details see [Inventory](./inventory.md)
|
||||||
|
|
||||||
`specialArgs`
|
`specialArgs`
|
||||||
: Extra arguments to pass to nixosSystem i.e. useful to make self available
|
: Extra arguments to pass to nixosSystem i.e. useful to make self available
|
||||||
@@ -54,61 +54,3 @@ The core function that produces a clan. It returns a set of consistent configura
|
|||||||
: A function that maps from architecture to pkgs, if specified this nixpkgs will be only imported once for each system.
|
: A function that maps from architecture to pkgs, if specified this nixpkgs will be only imported once for each system.
|
||||||
This improves performance, but all nipxkgs.* options will be ignored.
|
This improves performance, but all nipxkgs.* options will be ignored.
|
||||||
`(string -> pkgs )`
|
`(string -> pkgs )`
|
||||||
|
|
||||||
## Inventory
|
|
||||||
|
|
||||||
`Inventory` is an abstract service layer for consistently configuring distributed services across machine boundaries.
|
|
||||||
|
|
||||||
The following is the specification of the inventory in `cuelang`
|
|
||||||
|
|
||||||
```cue
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
// A name of the clan (primarily shown by the UI)
|
|
||||||
name: string
|
|
||||||
// A description of the clan
|
|
||||||
description?: string
|
|
||||||
// The icon path
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// A map of services
|
|
||||||
services: [string]: [string]: {
|
|
||||||
// Required meta fields
|
|
||||||
meta: {
|
|
||||||
name: string,
|
|
||||||
icon?: string
|
|
||||||
description?: string,
|
|
||||||
},
|
|
||||||
// Machines are added via the avilable roles
|
|
||||||
// Membership depends only on this field
|
|
||||||
roles: [string]: {
|
|
||||||
machines: [...string],
|
|
||||||
tags: [...string],
|
|
||||||
}
|
|
||||||
machines?: {
|
|
||||||
[string]: {
|
|
||||||
config?: {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Global Configuration for the service
|
|
||||||
// Applied to all machines.
|
|
||||||
config?: {
|
|
||||||
// Schema depends on the module.
|
|
||||||
// It declares the interface how the service can be configured.
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// A map of machines, extends the machines of `buildClan`
|
|
||||||
machines: [string]: {
|
|
||||||
name: string,
|
|
||||||
description?: string,
|
|
||||||
icon?: string
|
|
||||||
tags: [...string]
|
|
||||||
system: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
206
docs/site/concepts/inventory.md
Normal file
206
docs/site/concepts/inventory.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Inventory
|
||||||
|
|
||||||
|
`Inventory` is an abstract service layer for consistently configuring distributed services across machine boundaries.
|
||||||
|
|
||||||
|
## Meta
|
||||||
|
|
||||||
|
Metadata about the clan, will be displayed upfront in the upcomming clan-app, make sure to choose a unique name.
|
||||||
|
|
||||||
|
```{.nix hl_lines="3-8"}
|
||||||
|
buildClan {
|
||||||
|
inventory = {
|
||||||
|
meta = {
|
||||||
|
# The following options are available
|
||||||
|
# name: string # Required, name of the clan.
|
||||||
|
# description: null | string
|
||||||
|
# icon: null | string
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Machines
|
||||||
|
|
||||||
|
Machines and a small pieve of their configuration can be added via `inventory.machines`.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
It doesn't matter where the machine gets introduced to buildClan - All delarations are valid, duplications are merged.
|
||||||
|
|
||||||
|
However the clan-app (UI) will create machines in the inventory, because it cannot create arbitrary nixos configs.
|
||||||
|
|
||||||
|
In the following example `backup_server` is one machine - it may specify parts of its configuration in different places.
|
||||||
|
|
||||||
|
```{.nix hl_lines="3-5 12-20"}
|
||||||
|
buildClan {
|
||||||
|
machines = {
|
||||||
|
"backup_server" = {
|
||||||
|
# Any valid nixos config
|
||||||
|
};
|
||||||
|
"jon" = {
|
||||||
|
# Any valid nixos config
|
||||||
|
};
|
||||||
|
};
|
||||||
|
inventory = {
|
||||||
|
machines = {
|
||||||
|
"backup_server" = {
|
||||||
|
# Don't include any nixos config here
|
||||||
|
# The following fields are avilable
|
||||||
|
# description: null | string
|
||||||
|
# icon: null | string
|
||||||
|
# name: string
|
||||||
|
# system: null | string
|
||||||
|
# tags: [...string]
|
||||||
|
};
|
||||||
|
"jon" = {
|
||||||
|
# Same as above
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
### Available clanModules
|
||||||
|
|
||||||
|
Currently the inventory interface is implemented by the following clanModules
|
||||||
|
|
||||||
|
- [borgbackup](../reference/clanModules/borgbackup.md)
|
||||||
|
- [packages](../reference/clanModules/packages.md)
|
||||||
|
- [single-disk](../reference/clanModules/single-disk.md)
|
||||||
|
|
||||||
|
See the respective module documentation for available roles.
|
||||||
|
|
||||||
|
### Adding services to machines
|
||||||
|
|
||||||
|
A module can be added to one or multiple machines via `Roles`. clan's `Role` interface provide sane defaults for a module this allows the module author to reduce the configuration overhead to a minimum.
|
||||||
|
|
||||||
|
Each service can still be customized and configured according to the modules options.
|
||||||
|
|
||||||
|
- Per instance configuration via `services.<serviceName>.<instanceName>.config`
|
||||||
|
- Per machine configuration via `services.<serviceName>.<instanceName>.machines.<machineName>.config`
|
||||||
|
|
||||||
|
### Configuration Examples
|
||||||
|
|
||||||
|
!!! Example "Borgbackup Example"
|
||||||
|
|
||||||
|
To configure a service it needs to be added to the machine.
|
||||||
|
It is required to assign the service (`borgbackup`) an arbitrary instance name. (`instance_1`)
|
||||||
|
|
||||||
|
See also: [Multiple Service Instances](#multiple-service-instances)
|
||||||
|
|
||||||
|
```{.nix hl_lines="14-17"}
|
||||||
|
buildClan {
|
||||||
|
inventory = {
|
||||||
|
machines = {
|
||||||
|
"backup_server" = {
|
||||||
|
# Don't include any nixos config here
|
||||||
|
# See inventory.Machines for available options
|
||||||
|
};
|
||||||
|
"jon" = {
|
||||||
|
# Don't include any nixos config here
|
||||||
|
# See inventory.Machines for available options
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
borgbackup.instance_1 = {
|
||||||
|
roles.client.machines = [ "jon" ];
|
||||||
|
roles.server.machines = [ "backup_server" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Example "Packages Example"
|
||||||
|
|
||||||
|
This example shows how to add `pkgs.firefox` via the inventory interface.
|
||||||
|
|
||||||
|
```{.nix hl_lines="8-11"}
|
||||||
|
buildClan {
|
||||||
|
inventory = {
|
||||||
|
machines = {
|
||||||
|
"sara" = {};
|
||||||
|
"jon" = {};
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
packages.set_1 = {
|
||||||
|
roles.default.machines = [ "jon" "sara" ];
|
||||||
|
# Packages is a configuration option of the "packages" clanModule
|
||||||
|
config.packages = ["firefox"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tags
|
||||||
|
|
||||||
|
It is possible to add services to multiple machines via tags. The service instance gets added in the specified role. In this case `role = "default"`
|
||||||
|
|
||||||
|
!!! Example "Tags Example"
|
||||||
|
|
||||||
|
```{.nix hl_lines="5 8 13"}
|
||||||
|
buildClan {
|
||||||
|
inventory = {
|
||||||
|
machines = {
|
||||||
|
"sara" = {
|
||||||
|
tags = ["browsing"];
|
||||||
|
};
|
||||||
|
"jon" = {
|
||||||
|
tags = ["browsing"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
packages.set_1 = {
|
||||||
|
roles.default.tags = [ "browsing" ];
|
||||||
|
config.packages = ["firefox"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Service Instances
|
||||||
|
|
||||||
|
!!! danger "Important"
|
||||||
|
Not all modules support multiple instances yet.
|
||||||
|
|
||||||
|
Some modules have support for adding multiple instances of the same service in different roles or configurations.
|
||||||
|
|
||||||
|
!!! Example
|
||||||
|
|
||||||
|
In this example `backup_server` has role `client` and `server` in different instances.
|
||||||
|
|
||||||
|
```{.nix hl_lines="11 14"}
|
||||||
|
buildClan {
|
||||||
|
inventory = {
|
||||||
|
machines = {
|
||||||
|
"jon" = {};
|
||||||
|
"backup_server" = {};
|
||||||
|
"backup_backup_server" = {}
|
||||||
|
};
|
||||||
|
services = {
|
||||||
|
borgbackup.instance_1 = {
|
||||||
|
roles.client.machines = [ "jon" ];
|
||||||
|
roles.server.machines = [ "backup_server" ];
|
||||||
|
};
|
||||||
|
borgbackup.instance_1 = {
|
||||||
|
roles.client.machines = [ "backup_server" ];
|
||||||
|
roles.server.machines = [ "backup_backup_server" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema specification
|
||||||
|
|
||||||
|
The complete schema specification can be retrieved via:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix build git+https://git.clan.lol/clan/clan-core#inventory-schema
|
||||||
|
> result
|
||||||
|
> ├── schema.cue
|
||||||
|
> └── schema.json
|
||||||
|
```
|
||||||
@@ -22,87 +22,11 @@ in
|
|||||||
inherit lib;
|
inherit lib;
|
||||||
};
|
};
|
||||||
|
|
||||||
optionsFromModule =
|
getSchema = import ./interface-to-schema.nix { inherit lib self; };
|
||||||
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
|
in
|
||||||
{
|
{
|
||||||
legacyPackages.inventorySchema = schema;
|
legacyPackages.inventorySchema = getSchema { };
|
||||||
|
legacyPackages.inventorySchemaPretty = getSchema { includeDefaults = false; };
|
||||||
|
|
||||||
devShells.inventory-schema = pkgs.mkShell {
|
devShells.inventory-schema = pkgs.mkShell {
|
||||||
inputsFrom = with config.checks; [
|
inputsFrom = with config.checks; [
|
||||||
@@ -126,6 +50,19 @@ in
|
|||||||
cp schema.json $out
|
cp schema.json $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
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 self'.legacyPackages.inventorySchemaPretty)}
|
||||||
|
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 {
|
||||||
|
|||||||
98
lib/inventory/interface-to-schema.nix
Normal file
98
lib/inventory/interface-to-schema.nix
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{ 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 =
|
||||||
|
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;
|
||||||
|
}) (rolesOf moduleName)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
machines =
|
||||||
|
lib.recursiveUpdate
|
||||||
|
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.machines
|
||||||
|
{ additionalProperties.properties.config = moduleSchema; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
rolesOf =
|
||||||
|
moduleName:
|
||||||
|
let
|
||||||
|
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
|
||||||
|
schema
|
||||||
Reference in New Issue
Block a user