Inventory: init module merge & validation logic for inventory
This commit is contained in:
parent
69d88c8ba3
commit
f5a0fd7c9e
@ -1,6 +1,6 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
instances = config.clan.services.borgbackup;
|
instances = config.clan.inventory.services.borgbackup;
|
||||||
# roles = { ${role_name} :: { machines :: [string] } }
|
# roles = { ${role_name} :: { machines :: [string] } }
|
||||||
allServers = lib.foldlAttrs (
|
allServers = lib.foldlAttrs (
|
||||||
acc: _instanceName: instanceConfig:
|
acc: _instanceName: instanceConfig:
|
||||||
|
@ -4,7 +4,7 @@ let
|
|||||||
machineDir = clanDir + "/machines/";
|
machineDir = clanDir + "/machines/";
|
||||||
inherit (config.clan.core) machineName;
|
inherit (config.clan.core) machineName;
|
||||||
|
|
||||||
instances = config.clan.services.borgbackup;
|
instances = config.clan.inventory.services.borgbackup;
|
||||||
|
|
||||||
# roles = { ${role_name} :: { machines :: [string] } }
|
# roles = { ${role_name} :: { machines :: [string] } }
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ let
|
|||||||
inherit lib clan-core;
|
inherit lib clan-core;
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg = config.clan;
|
cfg = config.clan;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -91,6 +90,7 @@ in
|
|||||||
clanInternals = lib.mkOption {
|
clanInternals = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
|
inventory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
||||||
|
@ -7,40 +7,18 @@ The inventory is our concept for distributed services. Users can configure multi
|
|||||||
- Ensures that all machines and services are configured consistently, across multiple nixosConfigs.
|
- Ensures that all machines and services are configured consistently, across multiple nixosConfigs.
|
||||||
- Defaults and predefined roles in our modules minimizes the need for manual configuration.
|
- Defaults and predefined roles in our modules minimizes the need for manual configuration.
|
||||||
|
|
||||||
Design questions:
|
Open questions:
|
||||||
|
|
||||||
|
- [ ] How do we set default role, description and other metadata?
|
||||||
|
- It must be accessible from Python.
|
||||||
|
- It must set the value in the module system.
|
||||||
|
|
||||||
|
- [ ] Inventory might use assertions. Should each machine inherit the inventory assertions ?
|
||||||
|
|
||||||
- [ ] Is the service config interface the same as the module config interface ?
|
- [ ] Is the service config interface the same as the module config interface ?
|
||||||
|
|
||||||
- [ ] As a user i dont want to see borgbackup as the high level category ?
|
- [ ] As a user do I want to see borgbackup as the high level category?
|
||||||
|
|
||||||
- [x] Must roles be a list ?
|
|
||||||
-> Yes. In zerotier a machine can be "moon" and "controller" at the same time.
|
|
||||||
|
|
||||||
- [x] Is role client different from peer ? Do we have one example where we use client and peer together and they are different?
|
|
||||||
-> There are many roles. And they depend on the service.
|
|
||||||
|
|
||||||
- [x] Should we use the module name in the path of the service?
|
|
||||||
-> YES
|
|
||||||
```json
|
|
||||||
// ${module_name}.${instance_name}
|
|
||||||
services.borgbackup-static.backup1 = {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Pro:
|
|
||||||
Easier to handle.
|
|
||||||
Better groups the module specific instances.
|
|
||||||
Contra:
|
|
||||||
More nesting in json
|
|
||||||
|
|
||||||
Neutral: Module name is hard to change. Exists anyways.
|
|
||||||
|
|
||||||
- [x] Should the machine specific service config be part of the service?
|
|
||||||
-> NO. because ...
|
|
||||||
- The config implements the schema of the module, which is declared in the service.
|
|
||||||
- If the config is placed in the machine, it becomes unclear that the scope is ONLY the service and NOT the global nixos config.
|
|
||||||
- If the config is placed in the machine it is de-located into another top-level field. In the module this complicates access.
|
|
||||||
|
|
||||||
Architecture
|
Architecture
|
||||||
|
|
||||||
@ -55,27 +33,7 @@ nixos < borgbackup <- inventory <-> UI
|
|||||||
Defining Users is out of scope for the first prototype.
|
Defining Users is out of scope for the first prototype.
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Why do we need 2 modules?
|
## Provides a specification for the inventory
|
||||||
-> It is technically possible to have only 1 module.
|
|
||||||
Pros:
|
|
||||||
Simple to use/Easy to understand.
|
|
||||||
Less modules
|
|
||||||
Cons:
|
|
||||||
Harder to write a module. Because it must do 2 things.
|
|
||||||
One module should do only 1 thing.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
clan.machines.${machine_name} = {
|
|
||||||
# "borgbackup.ssh.pub" = machineDir + machines + "/facts/borgbackup.ssh.pub";
|
|
||||||
facts = ...
|
|
||||||
};
|
|
||||||
clan.services.${instance} = {
|
|
||||||
# roles.server = [ "jon_machine" ]
|
|
||||||
# roles.${role_name} = [ ${machine_name} ];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
This part provides a specification for the inventory.
|
|
||||||
|
|
||||||
It is used for design phase and as validation helper.
|
It is used for design phase and as validation helper.
|
||||||
|
|
||||||
|
@ -3,76 +3,25 @@ let
|
|||||||
clan-core = self;
|
clan-core = self;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Extension of the build clan interface
|
|
||||||
# new_clan = clan-core.lib.buildClan {
|
|
||||||
# # High level services.
|
|
||||||
# # If you need multiple instances of a service configure them via:
|
|
||||||
# # inventory.services.[serviceName].[instanceName] = ...
|
|
||||||
# services = {
|
|
||||||
# borbackup = {
|
|
||||||
# roles.server.machines = [ "vyr" ];
|
|
||||||
# roles.client.tags = [ "laptop" ];
|
|
||||||
# machines.vyr = {
|
|
||||||
# config = {
|
|
||||||
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# config = {
|
|
||||||
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
|
|
||||||
# # Low level inventory i.e. if you need multiple instances of a service
|
|
||||||
# # Or if you want to manipulate the created inventory directly.
|
|
||||||
# inventory.services.borbackup.default = { };
|
|
||||||
|
|
||||||
# # Machines. each machine can be referenced by its attribute name under services.
|
|
||||||
# machines = {
|
|
||||||
# camina = {
|
|
||||||
# # This is added to machine tags
|
|
||||||
# clan.tags = [ "laptop" ];
|
|
||||||
# # These are the inventory machine fields
|
|
||||||
# clan.meta.description = "";
|
|
||||||
# clan.meta.name = "";
|
|
||||||
# clan.meta.icon = "";
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# vyr = {
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# vi = {
|
|
||||||
# clan.networking.targetHost = "root@78.47.164.46";
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# aya = {
|
|
||||||
# clan.networking.targetHost = "root@78.47.164.46";
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# ezra = {
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# rianon = {
|
|
||||||
# # Config ...
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
|
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
meta.name = "sams's clans";
|
|
||||||
|
meta.name = "kenjis clan";
|
||||||
# Should usually point to the directory of flake.nix
|
# Should usually point to the directory of flake.nix
|
||||||
directory = self;
|
directory = self;
|
||||||
|
|
||||||
# services = {
|
# service config
|
||||||
# borgbackup = {
|
# Useful alias: "inventory.services.borgbackup.default"
|
||||||
# roles.server.machines = [ "vyr_machine" ];
|
services = {
|
||||||
# roles.client.tags = [ "laptop" ];
|
borgbackup = {
|
||||||
# };
|
roles.server.machines = [ "vyr_machine" ];
|
||||||
# };
|
roles.client.tags = [ "laptop" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# OR
|
# merged with
|
||||||
inventory = builtins.fromJSON (builtins.readFile ./src/tests/borgbackup.json);
|
inventory = builtins.fromJSON (builtins.readFile ./src/tests/borgbackup.json);
|
||||||
|
|
||||||
|
# merged with
|
||||||
machines = {
|
machines = {
|
||||||
"vyr_machine" = { };
|
"vyr_machine" = { };
|
||||||
"vi_machine" = {
|
"vi_machine" = {
|
||||||
|
@ -31,18 +31,32 @@
|
|||||||
inventory ? { },
|
inventory ? { },
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
_inventory =
|
# Internal inventory, this is the result of merging all potential inventory sources:
|
||||||
(
|
# - Default instances configured via 'services'
|
||||||
if services != { } && inventory == { } then
|
# - The inventory overrides
|
||||||
{ services = lib.mapAttrs (_name: value: { default = value; }) services; }
|
# - Machines that exist in inventory.machines
|
||||||
else if services == { } && inventory != { } then
|
# - Machines explicitly configured via 'machines' argument
|
||||||
|
# - Machines that exist in the machines directory
|
||||||
|
# Checks on the module level:
|
||||||
|
# - Each service role must reference a valid machine after all machines are merged
|
||||||
|
mergedInventory =
|
||||||
|
(lib.evalModules {
|
||||||
|
modules = [
|
||||||
|
./interface.nix
|
||||||
|
{ inherit meta; }
|
||||||
|
# Default instances configured via 'services'
|
||||||
|
{
|
||||||
|
services = lib.mapAttrs (_name: value: {
|
||||||
|
default = value // {
|
||||||
|
meta.name = lib.mkDefault _name;
|
||||||
|
};
|
||||||
|
}) services;
|
||||||
|
}
|
||||||
|
# The inventory overrides
|
||||||
inventory
|
inventory
|
||||||
else if services != { } && inventory != { } then
|
# Machines explicitly configured via 'machines' argument
|
||||||
throw "Either services or inventory should be set, but not both."
|
{
|
||||||
else
|
# { ${name} :: meta // { name, tags } }
|
||||||
{ }
|
|
||||||
)
|
|
||||||
// {
|
|
||||||
machines = lib.mapAttrs (
|
machines = lib.mapAttrs (
|
||||||
name: config:
|
name: config:
|
||||||
(lib.attrByPath [
|
(lib.attrByPath [
|
||||||
@ -50,7 +64,8 @@ let
|
|||||||
"meta"
|
"meta"
|
||||||
] { } config)
|
] { } config)
|
||||||
// {
|
// {
|
||||||
name = (
|
# meta.name default is the attribute name of the machine
|
||||||
|
name = lib.mkDefault (
|
||||||
lib.attrByPath [
|
lib.attrByPath [
|
||||||
"clan"
|
"clan"
|
||||||
"meta"
|
"meta"
|
||||||
@ -63,27 +78,15 @@ let
|
|||||||
] [ ] config;
|
] [ ] config;
|
||||||
}
|
}
|
||||||
) machines;
|
) machines;
|
||||||
};
|
}
|
||||||
|
# Machines that exist in the machines directory
|
||||||
|
{ machines = lib.mapAttrs (name: _: { inherit name; }) machinesDirs; }
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
|
||||||
buildInventory = import ./inventory.nix { inherit lib clan-core; };
|
buildInventory = import ./inventory.nix { inherit lib clan-core; };
|
||||||
|
|
||||||
pkgs = import nixpkgs { };
|
serviceConfigs = buildInventory mergedInventory;
|
||||||
|
|
||||||
inventoryFile = builtins.toFile "inventory.json" (builtins.toJSON _inventory);
|
|
||||||
|
|
||||||
# a Derivation that can be forced to validate the inventory
|
|
||||||
# It is not used directly here.
|
|
||||||
validatedFile = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "validated-inventory";
|
|
||||||
src = ../../inventory/src;
|
|
||||||
buildInputs = [ pkgs.cue ];
|
|
||||||
installPhase = ''
|
|
||||||
cue vet ${inventoryFile} root.cue -d "#Root"
|
|
||||||
cp ${inventoryFile} $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
serviceConfigs = buildInventory _inventory;
|
|
||||||
|
|
||||||
deprecationWarnings = [
|
deprecationWarnings = [
|
||||||
(lib.warnIf (
|
(lib.warnIf (
|
||||||
@ -167,6 +170,8 @@ let
|
|||||||
clan-core.nixosModules.clanCore
|
clan-core.nixosModules.clanCore
|
||||||
extraConfig
|
extraConfig
|
||||||
(machines.${name} or { })
|
(machines.${name} or { })
|
||||||
|
# Inherit the inventory assertions ?
|
||||||
|
{ inherit (mergedInventory) assertions; }
|
||||||
{ imports = serviceConfigs.${name} or { }; }
|
{ imports = serviceConfigs.${name} or { }; }
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@ -250,7 +255,7 @@ builtins.deepSeq deprecationWarnings {
|
|||||||
# Evaluated clan meta
|
# Evaluated clan meta
|
||||||
# Merged /clan/meta.json with overrides from buildClan
|
# Merged /clan/meta.json with overrides from buildClan
|
||||||
meta = mergedMeta;
|
meta = mergedMeta;
|
||||||
inherit _inventory validatedFile;
|
inventory = mergedInventory;
|
||||||
|
|
||||||
# machine specifics
|
# machine specifics
|
||||||
machines = configsPerSystem;
|
machines = configsPerSystem;
|
||||||
|
120
lib/build-clan/interface.nix
Normal file
120
lib/build-clan/interface.nix
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
t = lib.types;
|
||||||
|
|
||||||
|
metaOptions = {
|
||||||
|
name = lib.mkOption { type = t.str; };
|
||||||
|
description = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
type = t.nullOr t.str;
|
||||||
|
};
|
||||||
|
icon = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
type = t.nullOr t.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
machineRef = lib.mkOptionType {
|
||||||
|
name = "machineRef";
|
||||||
|
description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]";
|
||||||
|
check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines);
|
||||||
|
merge = lib.mergeEqualOption;
|
||||||
|
};
|
||||||
|
|
||||||
|
allTags = lib.unique (
|
||||||
|
lib.foldlAttrs (
|
||||||
|
tags: _: m:
|
||||||
|
tags ++ m.tags or [ ]
|
||||||
|
) [ ] config.machines
|
||||||
|
);
|
||||||
|
|
||||||
|
tagRef = lib.mkOptionType {
|
||||||
|
name = "tagRef";
|
||||||
|
description = "Tags :: [${builtins.concatStringsSep " | " allTags}]";
|
||||||
|
check = v: lib.isString v && builtins.elem v allTags;
|
||||||
|
merge = lib.mergeEqualOption;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.assertions = lib.mkOption {
|
||||||
|
type = t.listOf t.unspecified;
|
||||||
|
internal = true;
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
config.assertions = lib.foldlAttrs (
|
||||||
|
ass1: serviceName: c:
|
||||||
|
ass1
|
||||||
|
++ lib.foldlAttrs (
|
||||||
|
ass2: instanceName: instanceConfig:
|
||||||
|
let
|
||||||
|
serviceMachineNames = lib.attrNames instanceConfig.machines;
|
||||||
|
topLevelMachines = lib.attrNames config.machines;
|
||||||
|
# All machines must be defined in the top-level machines
|
||||||
|
assertions = builtins.map (m: {
|
||||||
|
assertion = builtins.elem m topLevelMachines;
|
||||||
|
message = "${serviceName}.${instanceName}.machines.${m}. Should be one of [ ${builtins.concatStringsSep " | " topLevelMachines} ]";
|
||||||
|
}) serviceMachineNames;
|
||||||
|
in
|
||||||
|
ass2 ++ assertions
|
||||||
|
) [ ] c
|
||||||
|
) [ ] config.services;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
options.services = lib.mkOption {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -90,7 +90,7 @@ let
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
config.clan.services.${moduleName}.${instanceName} = {
|
config.clan.inventory.services.${moduleName}.${instanceName} = {
|
||||||
roles = resolvedRoles;
|
roles = resolvedRoles;
|
||||||
# TODO: Add inverseRoles to the service config if needed
|
# TODO: Add inverseRoles to the service config if needed
|
||||||
# inherit inverseRoles;
|
# inherit inverseRoles;
|
||||||
|
@ -29,9 +29,7 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# clan.inventory.${moduleName}.${instanceName} = { ... }
|
options.clan.inventory.services = lib.mkOption {
|
||||||
# TODO: resolve clash with clan.services.waypipe
|
|
||||||
options.clan.services = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.attrsOf instanceOptions);
|
type = lib.types.attrsOf (lib.types.attrsOf instanceOptions);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user