Extend build-clan interface

This commit is contained in:
Johannes Kirschbauer 2024-06-21 22:46:12 +02:00 committed by hsjobeki
parent e44b07df66
commit c89080deb4
7 changed files with 257 additions and 164 deletions

View File

@ -7,11 +7,12 @@ let
instances = config.clan.services.borgbackup; instances = config.clan.services.borgbackup;
# roles = { ${role_name} :: { machines :: [string] } } # roles = { ${role_name} :: { machines :: [string] } }
allClients = lib.foldlAttrs ( allClients = lib.foldlAttrs (
acc: _instanceName: instanceConfig: acc: _instanceName: instanceConfig:
acc acc
++ ( ++ (
if builtins.elem machineName instanceConfig.roles.server.machines then if (builtins.elem machineName instanceConfig.roles.server.machines) then
instanceConfig.roles.client.machines instanceConfig.roles.client.machines
else else
[ ] [ ]
@ -21,7 +22,6 @@ in
{ {
config.services.borgbackup.repos = config.services.borgbackup.repos =
let let
borgbackupIpMachinePath = machines: machineDir + machines + "/facts/borgbackup.ssh.pub"; borgbackupIpMachinePath = machines: machineDir + machines + "/facts/borgbackup.ssh.pub";
machinesMaybeKey = builtins.map ( machinesMaybeKey = builtins.map (
machine: machine:

View File

@ -1,183 +1,87 @@
{ self, lib, ... }: { self, lib, ... }:
let let
clan-core = self; clan-core = self;
# syncthing_inventory = builtins.fromJSON (builtins.readFile ./src/tests/syncthing.json);
syncthing_inventory = builtins.fromJSON (builtins.readFile ./src/tests/borgbackup.json);
machines = machinesFromInventory syncthing_inventory;
resolveTags =
# Inventory, { machines :: [string], tags :: [string] }
inventory: members: {
machines =
members.machines or [ ]
++ (builtins.foldl' (
acc: tag:
let
tagMembers = builtins.attrNames (
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
);
in
# throw "Machine tag ${tag} not found. Not machine with: tag ${tagName} not in inventory.";
if tagMembers == [ ] then
throw "Machine tag ${tag} not found. Not machine with: tag ${tag} not in inventory."
else
acc ++ tagMembers
) [ ] members.tags or [ ]);
};
/*
Returns a NixOS configuration for every machine in the inventory.
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
*/
machinesFromInventory =
inventory:
# For every machine in the inventory, build a NixOS configuration
# For each machine generate config, forEach service, if the machine is used.
builtins.mapAttrs (
machineName: _:
lib.foldlAttrs (
# [ Modules ], String, { ${instance_name} :: ServiceConfig }
acc: moduleName: serviceConfigs:
acc
# Collect service config
++ (lib.foldlAttrs (
# [ Modules ], String, ServiceConfig
acc2: instanceName: serviceConfig:
let
resolvedRoles = builtins.mapAttrs (
_roleName: members: resolveTags inventory members
) serviceConfig.roles;
isInService = builtins.any (members: builtins.elem machineName members.machines) (
builtins.attrValues resolvedRoles
);
# Inverse map of roles. Allows for easy lookup of roles for a given machine.
# { ${machine_name} :: [roles]
inverseRoles = lib.foldlAttrs (
acc: roleName:
{ machines }:
acc
// builtins.foldl' (
acc2: machineName: acc2 // { ${machineName} = (acc.${machineName} or [ ]) ++ [ roleName ]; }
) { } machines
) { } resolvedRoles;
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
globalConfig = serviceConfig.config;
# TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy
roleModules = builtins.map (
role:
let
path = "${clan-core.clanModules.${moduleName}}/roles/${role}.nix";
in
if builtins.pathExists path then
path
else
throw "Role doesnt have a module: ${role}. Path: ${path} not found."
) inverseRoles.${machineName} or [ ];
in
if isInService then
acc2
++ [
{
imports = [ clan-core.clanModules.${moduleName} ] ++ roleModules;
config.clan.${moduleName} = lib.mkMerge [
globalConfig
machineServiceConfig
];
}
{
config.clan.services.${moduleName}.${instanceName} = {
roles = resolvedRoles;
# inherit inverseRoles;
};
}
]
else
acc2
) [ ] serviceConfigs)
) [ ] inventory.services
) inventory.machines;
in in
{ {
inherit clan-core;
# Extension of the build clan interface # Extension of the build clan interface
new_clan = clan-core.lib.buildClan { # new_clan = clan-core.lib.buildClan {
# High level services. # # High level services.
# If you need multiple instances of a service configure them via: # # If you need multiple instances of a service configure them via:
# inventory.services.[serviceName].[instanceName] = ... # # inventory.services.[serviceName].[instanceName] = ...
services = { # services = {
borbackup = { # borbackup = {
roles.server.machines = [ "vyr" ]; # roles.server.machines = [ "vyr" ];
roles.client.tags = [ "laptop" ]; # roles.client.tags = [ "laptop" ];
machines.vyr = { # machines.vyr = {
config = { # config = {
}; # };
}; # };
config = { # config = {
}; # };
}; # };
}; # };
# Low level inventory i.e. if you need multiple instances of a service # # Low level inventory i.e. if you need multiple instances of a service
# Or if you want to manipulate the created inventory directly. # # Or if you want to manipulate the created inventory directly.
inventory.services.borbackup.default = { }; # inventory.services.borbackup.default = { };
# Machines. each machine can be referenced by its attribute name under services. # # Machines. each machine can be referenced by its attribute name under services.
machines = { # machines = {
camina = { # camina = {
# This is added to machine tags # # This is added to machine tags
clan.tags = [ "laptop" ]; # clan.tags = [ "laptop" ];
# These are the inventory machine fields # # These are the inventory machine fields
clan.meta.description = ""; # clan.meta.description = "";
clan.meta.name = ""; # clan.meta.name = "";
clan.meta.icon = ""; # clan.meta.icon = "";
# Config ... # # Config ...
}; # };
vyr = { # vyr = {
# Config ... # # Config ...
}; # };
vi = { # vi = {
clan.networking.targetHost = "root@78.47.164.46"; # clan.networking.targetHost = "root@78.47.164.46";
# Config ... # # Config ...
}; # };
aya = { # aya = {
clan.networking.targetHost = "root@78.47.164.46"; # clan.networking.targetHost = "root@78.47.164.46";
# Config ... # # Config ...
}; # };
ezra = { # ezra = {
# Config ... # # Config ...
}; # };
rianon = { # rianon = {
# Config ... # # Config ...
}; # };
}; # };
}; # };
clan = clan-core.lib.buildClan { clan = clan-core.lib.buildClan {
meta.name = "vi's clans"; meta.name = "sams's clans";
# Should usually point to the directory of flake.nix # Should usually point to the directory of flake.nix
directory = self; directory = self;
# services = {
# borgbackup = {
# roles.server.machines = [ "vyr_machine" ];
# roles.client.tags = [ "laptop" ];
# };
# };
# OR
inventory = builtins.fromJSON (builtins.readFile ./src/tests/borgbackup.json);
machines = { machines = {
"vyr_machine" = { };
"vi_machine" = { "vi_machine" = {
imports = machines.vi_machine; clan.tags = [ "laptop" ];
};
"vyr_machine" = {
imports = machines.vyr_machine;
}; };
"camina_machine" = { "camina_machine" = {
imports = machines.camina_machine; clan.tags = [ "laptop" ];
clan.meta.name = "camina";
}; };
}; };
}; };
intern = machines;
} }

View File

@ -12,10 +12,79 @@
# DEPRECATED: use meta.icon instead # DEPRECATED: use meta.icon instead
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system. # A map from arch 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.
pkgsForSystem ? (_system: null),
/*
Distributed services configuration.
This configures a default instance in the inventory with the name "default".
If you need multiple instances of a service configure them via:
inventory.services.[serviceName].[instanceName] = ...
*/
services ? { },
/*
Low level inventory configuration.
Overrides the services configuration.
*/
inventory ? { },
}: }:
let let
_inventory =
(
if services != { } && inventory == { } then
{ services = lib.mapAttrs (_name: value: { default = value; }) services; }
else if services == { } && inventory != { } then
inventory
else if services != { } && inventory != { } then
throw "Either services or inventory should be set, but not both."
else
{ }
)
// {
machines = lib.mapAttrs (
name: config:
(lib.attrByPath [
"clan"
"meta"
] { } config)
// {
name = (
lib.attrByPath [
"clan"
"meta"
"name"
] name config
);
tags = lib.attrByPath [
"clan"
"tags"
] [ ] config;
}
) machines;
};
buildInventory = import ./inventory.nix { inherit lib clan-core; };
pkgs = import nixpkgs { };
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 (
clanName != null clanName != null
@ -98,6 +167,7 @@ let
clan-core.nixosModules.clanCore clan-core.nixosModules.clanCore
extraConfig extraConfig
(machines.${name} or { }) (machines.${name} or { })
{ imports = serviceConfigs.${name} or { }; }
( (
{ {
# Settings # Settings
@ -180,6 +250,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;
# machine specifics # machine specifics
machines = configsPerSystem; machines = configsPerSystem;

View File

@ -0,0 +1,106 @@
# Generate partial NixOS configurations for every machine in the inventory
# This function is responsible for generating the module configuration for every machine in the inventory.
{ lib, clan-core }:
inventory:
let
machines = machinesFromInventory inventory;
resolveTags =
# Inventory, { machines :: [string], tags :: [string] }
inventory: members: {
machines =
members.machines or [ ]
++ (builtins.foldl' (
acc: tag:
let
tagMembers = builtins.attrNames (
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
);
in
# throw "Machine tag ${tag} not found. Not machine with: tag ${tagName} not in inventory.";
if tagMembers == [ ] then
throw "Machine tag ${tag} not found. Not machine with: tag ${tag} not in inventory."
else
acc ++ tagMembers
) [ ] members.tags or [ ]);
};
/*
Returns a NixOS configuration for every machine in the inventory.
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
*/
machinesFromInventory =
inventory:
# For every machine in the inventory, build a NixOS configuration
# For each machine generate config, forEach service, if the machine is used.
builtins.mapAttrs (
machineName: _:
lib.foldlAttrs (
# [ Modules ], String, { ${instance_name} :: ServiceConfig }
acc: moduleName: serviceConfigs:
acc
# Collect service config
++ (lib.foldlAttrs (
# [ Modules ], String, ServiceConfig
acc2: instanceName: serviceConfig:
let
resolvedRoles = builtins.mapAttrs (
_roleName: members: resolveTags inventory members
) serviceConfig.roles;
isInService = builtins.any (members: builtins.elem machineName members.machines) (
builtins.attrValues resolvedRoles
);
# Inverse map of roles. Allows for easy lookup of roles for a given machine.
# { ${machine_name} :: [roles]
inverseRoles = lib.foldlAttrs (
acc: roleName:
{ machines }:
acc
// builtins.foldl' (
acc2: machineName: acc2 // { ${machineName} = (acc.${machineName} or [ ]) ++ [ roleName ]; }
) { } machines
) { } resolvedRoles;
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
globalConfig = serviceConfig.config or { };
# TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy
roleModules = builtins.map (
role:
let
path = "${clan-core.clanModules.${moduleName}}/roles/${role}.nix";
in
if builtins.pathExists path then
path
else
throw "Role doesnt have a module: ${role}. Path: ${path} not found."
) inverseRoles.${machineName} or [ ];
in
if isInService then
acc2
++ [
{
imports = [ clan-core.clanModules.${moduleName} ] ++ roleModules;
config.clan.${moduleName} = lib.mkMerge [
globalConfig
machineServiceConfig
];
}
{
config.clan.services.${moduleName}.${instanceName} = {
roles = resolvedRoles;
# TODO: Add inverseRoles to the service config if needed
# inherit inverseRoles;
};
}
]
else
acc2
) [ ] serviceConfigs)
) [ ] inventory.services
) inventory.machines;
in
machines

View File

@ -16,5 +16,6 @@
./zerotier ./zerotier
# Inventory # Inventory
./inventory/interface.nix ./inventory/interface.nix
./meta/interface.nix
]; ];
} }

View File

@ -30,6 +30,7 @@ let
in in
{ {
# clan.inventory.${moduleName}.${instanceName} = { ... } # clan.inventory.${moduleName}.${instanceName} = { ... }
# TODO: resolve clash with clan.services.waypipe
options.clan.services = lib.mkOption { options.clan.services = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf instanceOptions); type = lib.types.attrsOf (lib.types.attrsOf instanceOptions);
}; };

View File

@ -0,0 +1,10 @@
{ lib, ... }:
let
optStr = lib.types.nullOr lib.types.str;
in
{
options.clan.meta.name = lib.mkOption { type = lib.types.str; };
options.clan.meta.description = lib.mkOption { type = optStr; };
options.clan.meta.icon = lib.mkOption { type = optStr; };
options.clan.tags = lib.mkOption { type = lib.types.listOf lib.types.str; };
}