feat(clan-services): enable recursive services
Using recursive services is potentially complex and requires carefully designed services. Nested Services create nixos modules which must be mergable as always.
This commit is contained in:
@@ -26,8 +26,6 @@ let
|
|||||||
|
|
||||||
The caller is responsible to use .config or .extendModules
|
The caller is responsible to use .config or .extendModules
|
||||||
*/
|
*/
|
||||||
# TODO: evaluate against the role.settings statically and use extendModules to get the machineSettings
|
|
||||||
# Doing this might improve performance
|
|
||||||
evalMachineSettings =
|
evalMachineSettings =
|
||||||
{
|
{
|
||||||
roleName,
|
roleName,
|
||||||
@@ -91,15 +89,12 @@ let
|
|||||||
instanceName: instance:
|
instanceName: instance:
|
||||||
lib.mapAttrs (roleName: role: {
|
lib.mapAttrs (roleName: role: {
|
||||||
machines = lib.mapAttrs (machineName: v: {
|
machines = lib.mapAttrs (machineName: v: {
|
||||||
# TODO: evaluate the settings against the interface
|
|
||||||
# settings = (evalMachineSettings { inherit roleName instanceName; inherit (v) settings; }).config;
|
|
||||||
settings =
|
settings =
|
||||||
(evalMachineSettings {
|
(evalMachineSettings {
|
||||||
inherit roleName instanceName machineName;
|
inherit roleName instanceName machineName;
|
||||||
inherit (v) settings;
|
inherit (v) settings;
|
||||||
}).config;
|
}).config;
|
||||||
}) role.machines;
|
}) role.machines;
|
||||||
# TODO: evaluate the settings against the interface
|
|
||||||
settings =
|
settings =
|
||||||
(evalMachineSettings {
|
(evalMachineSettings {
|
||||||
inherit roleName instanceName;
|
inherit roleName instanceName;
|
||||||
@@ -140,11 +135,6 @@ in
|
|||||||
(
|
(
|
||||||
{ name, ... }:
|
{ name, ... }:
|
||||||
{
|
{
|
||||||
# options.settings = mkOption {
|
|
||||||
# description = "settings of 'instance': ${name}";
|
|
||||||
# default = {};
|
|
||||||
# apply = v: lib.seq (checkInstanceSettings name v) v;
|
|
||||||
# };
|
|
||||||
options.roles = mkOption {
|
options.roles = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Roles of the instance.
|
Roles of the instance.
|
||||||
@@ -328,8 +318,6 @@ in
|
|||||||
- *defaults* that depend on the *machine* or *instance* should be added to *settings* later in 'perInstance' or 'perMachine'
|
- *defaults* that depend on the *machine* or *instance* should be added to *settings* later in 'perInstance' or 'perMachine'
|
||||||
'';
|
'';
|
||||||
type = types.deferredModule;
|
type = types.deferredModule;
|
||||||
# TODO: Default to an empty module
|
|
||||||
# need to test that an the empty module can be evaluated to empty settings
|
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
options.perInstance = mkOption {
|
options.perInstance = mkOption {
|
||||||
@@ -424,7 +412,6 @@ in
|
|||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
# TODO: Recursive services
|
|
||||||
options.services = mkOption {
|
options.services = mkOption {
|
||||||
visible = false;
|
visible = false;
|
||||||
type = attrsWith {
|
type = attrsWith {
|
||||||
@@ -444,7 +431,6 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
apply = _: throw "Not implemented yet";
|
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -551,7 +537,6 @@ in
|
|||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
# TODO: Recursive services
|
|
||||||
options.services = mkOption {
|
options.services = mkOption {
|
||||||
visible = false;
|
visible = false;
|
||||||
type = attrsWith {
|
type = attrsWith {
|
||||||
@@ -569,7 +554,6 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
apply = _: throw "Not implemented yet";
|
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -603,7 +587,6 @@ in
|
|||||||
in
|
in
|
||||||
uniqueStrings (collectRoles machineScope.instances);
|
uniqueStrings (collectRoles machineScope.instances);
|
||||||
};
|
};
|
||||||
# TODO: instances.<instanceName>.roles should contain all roles, even if nobody has the role
|
|
||||||
inherit (machineScope) instances;
|
inherit (machineScope) instances;
|
||||||
|
|
||||||
# There are no machine settings.
|
# There are no machine settings.
|
||||||
@@ -641,7 +624,7 @@ in
|
|||||||
allMachines :: {
|
allMachines :: {
|
||||||
<machineName> :: {
|
<machineName> :: {
|
||||||
nixosModule :: NixOSModule;
|
nixosModule :: NixOSModule;
|
||||||
services :: { }; # TODO: nested services
|
services :: { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -680,6 +663,7 @@ in
|
|||||||
type = types.attrsOf types.raw;
|
type = types.attrsOf types.raw;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# The result collected from 'perMachine'
|
||||||
result.allMachines = mkOption {
|
result.allMachines = mkOption {
|
||||||
visible = false;
|
visible = false;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
@@ -734,13 +718,40 @@ in
|
|||||||
default = lib.mapAttrs (
|
default = lib.mapAttrs (
|
||||||
machineName: machineResult:
|
machineName: machineResult:
|
||||||
let
|
let
|
||||||
instanceResults = lib.foldlAttrs (
|
instanceResults =
|
||||||
acc: roleName: role:
|
lib.foldlAttrs
|
||||||
acc
|
(
|
||||||
++ lib.foldlAttrs (
|
roleAcc: roleName: role:
|
||||||
acc: instanceName: instance:
|
roleAcc
|
||||||
|
// lib.foldlAttrs (
|
||||||
|
instanceAcc: instanceName: instance:
|
||||||
|
instanceAcc
|
||||||
|
// {
|
||||||
|
nixosModules =
|
||||||
|
(
|
||||||
|
(lib.mapAttrsToList (
|
||||||
|
nestedServiceName: serviceModule:
|
||||||
|
let
|
||||||
|
unmatchedMachines = lib.attrNames (
|
||||||
|
lib.removeAttrs serviceModule.result.final (lib.attrNames config.result.allMachines)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
if unmatchedMachines != [ ] then
|
||||||
|
throw ''
|
||||||
|
The following machines are not part of the parent service: ${builtins.toJSON unmatchedMachines}
|
||||||
|
Either remove the machines, or include them into the parent via a role.
|
||||||
|
(Added via roles.${roleName}.perInstance.services.${nestedServiceName})
|
||||||
|
|
||||||
|
${errorContext}
|
||||||
|
''
|
||||||
|
else
|
||||||
|
serviceModule.result.final.${machineName}.nixosModule
|
||||||
|
) instance.allMachines.${machineName}.services)
|
||||||
|
|
||||||
|
)
|
||||||
|
++ (
|
||||||
if instance.allMachines.${machineName}.nixosModule or { } != { } then
|
if instance.allMachines.${machineName}.nixosModule or { } != { } then
|
||||||
acc
|
instanceAcc.nixosModules
|
||||||
++ [
|
++ [
|
||||||
(lib.setDefaultModuleLocation
|
(lib.setDefaultModuleLocation
|
||||||
"Via instances.${instanceName}.roles.${roleName}.machines.${machineName}"
|
"Via instances.${instanceName}.roles.${roleName}.machines.${machineName}"
|
||||||
@@ -748,14 +759,22 @@ in
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
acc
|
instanceAcc.nixosModules
|
||||||
) [ ] role.allInstances
|
);
|
||||||
) [ ] config.result.allRoles;
|
}
|
||||||
|
) roleAcc role.allInstances
|
||||||
|
)
|
||||||
|
{
|
||||||
|
nixosModules = [ ];
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
config.result.allRoles;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit instanceResults;
|
inherit instanceResults machineResult;
|
||||||
nixosModule = {
|
nixosModule = {
|
||||||
imports = [
|
imports =
|
||||||
|
[
|
||||||
# include service assertions:
|
# include service assertions:
|
||||||
(
|
(
|
||||||
let
|
let
|
||||||
@@ -765,12 +784,27 @@ in
|
|||||||
assertions = lib.attrValues failedAssertions;
|
assertions = lib.attrValues failedAssertions;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
(lib.setDefaultModuleLocation "Via ${config.manifest.name}.perMachine - machine='${machineName}';" machineResult.nixosModule)
|
||||||
|
]
|
||||||
|
++ (lib.mapAttrsToList (
|
||||||
|
nestedServiceName: serviceModule:
|
||||||
|
let
|
||||||
|
unmatchedMachines = lib.attrNames (
|
||||||
|
lib.removeAttrs serviceModule.result.final (lib.attrNames config.result.allMachines)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
if unmatchedMachines != [ ] then
|
||||||
|
throw ''
|
||||||
|
The following machines are not part of the parent service: ${builtins.toJSON unmatchedMachines}
|
||||||
|
Either remove the machines, or include them into the parent via a role.
|
||||||
|
(Added via perMachine.services.${nestedServiceName})
|
||||||
|
|
||||||
# For error backtracing. This module was produced by the 'perMachine' function
|
${errorContext}
|
||||||
# TODO: check if we need this or if it leads to better errors if we pass the underlying module locations
|
''
|
||||||
# (lib.setDefaultModuleLocation "clan.service: ${config.manifest.name} - via perMachine" machineResult.nixosModule)
|
else
|
||||||
(machineResult.nixosModule)
|
serviceModule.result.final.${machineName}.nixosModule
|
||||||
] ++ instanceResults;
|
) machineResult.services)
|
||||||
|
++ instanceResults.nixosModules;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
) config.result.allMachines;
|
) config.result.allMachines;
|
||||||
|
|||||||
@@ -278,4 +278,5 @@ in
|
|||||||
|
|
||||||
per_machine_args = import ./per_machine_args.nix { inherit lib callInventoryAdapter; };
|
per_machine_args = import ./per_machine_args.nix { inherit lib callInventoryAdapter; };
|
||||||
per_instance_args = import ./per_instance_args.nix { inherit lib callInventoryAdapter; };
|
per_instance_args = import ./per_instance_args.nix { inherit lib callInventoryAdapter; };
|
||||||
|
nested = import ./nested_services { inherit lib clanLib; };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{ clanLib, lib, ... }:
|
||||||
|
{
|
||||||
|
test_simple = import ./simple.nix { inherit clanLib lib; };
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
service-B :: Service
|
||||||
|
exports a nixosModule which set "address" and "hostname"
|
||||||
|
Note: How we use null together with mkIf to create optional values.
|
||||||
|
This is a method, to create mergable modules
|
||||||
|
|
||||||
|
service-A :: Service
|
||||||
|
|
||||||
|
service-A.roles.server.perInstance.services."B"
|
||||||
|
imports service-B
|
||||||
|
configures a client with hostname = "johnny"
|
||||||
|
|
||||||
|
service-A.perMachine.services."B"
|
||||||
|
imports service-B
|
||||||
|
configures a client with address = "root"
|
||||||
|
*/
|
||||||
|
{ clanLib, lib, ... }:
|
||||||
|
let
|
||||||
|
service-B = (
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
manifest.name = "service-B";
|
||||||
|
|
||||||
|
roles.client.interface = {
|
||||||
|
options.hostname = lib.mkOption { default = null; };
|
||||||
|
options.address = lib.mkOption { default = null; };
|
||||||
|
};
|
||||||
|
roles.client.perInstance =
|
||||||
|
{ settings, ... }:
|
||||||
|
{
|
||||||
|
nixosModule = {
|
||||||
|
imports = [
|
||||||
|
# Only export the value that is actually set.
|
||||||
|
(lib.mkIf (settings.hostname != null) {
|
||||||
|
hostname = settings.hostname;
|
||||||
|
})
|
||||||
|
(lib.mkIf (settings.address != null) {
|
||||||
|
address = settings.address;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
service-A =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
manifest.name = "service-A";
|
||||||
|
|
||||||
|
instances.foo = {
|
||||||
|
roles.server.machines."jon" = { };
|
||||||
|
};
|
||||||
|
instances.bar = {
|
||||||
|
roles.server.machines."jon" = { };
|
||||||
|
};
|
||||||
|
|
||||||
|
roles.server = {
|
||||||
|
perInstance =
|
||||||
|
{ machine, instanceName, ... }:
|
||||||
|
{
|
||||||
|
services."B" = {
|
||||||
|
imports = [
|
||||||
|
service-B
|
||||||
|
];
|
||||||
|
instances."B-for-A" = {
|
||||||
|
roles.client.machines.${machine.name} = {
|
||||||
|
settings.hostname = instanceName + "+johnny";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
perMachine =
|
||||||
|
{ machine, ... }:
|
||||||
|
{
|
||||||
|
services."B" = {
|
||||||
|
imports = [
|
||||||
|
service-B
|
||||||
|
];
|
||||||
|
instances."B-for-A" = {
|
||||||
|
roles.client.machines.${machine.name} = {
|
||||||
|
settings.address = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
eval = clanLib.inventory.evalClanService {
|
||||||
|
modules = [
|
||||||
|
(service-A)
|
||||||
|
];
|
||||||
|
prefix = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
evalNixos = lib.evalModules {
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
options.assertions = lib.mkOption { };
|
||||||
|
options.hostname = lib.mkOption { type = lib.types.separatedString " "; };
|
||||||
|
options.address = lib.mkOption { type = lib.types.str; };
|
||||||
|
}
|
||||||
|
eval.config.result.final."jon".nixosModule
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Check that the nixos system has the settings from the nested module, as well as those from the "perMachine" and "perInstance"
|
||||||
|
inherit eval;
|
||||||
|
expr = evalNixos.config;
|
||||||
|
expected = {
|
||||||
|
address = "root";
|
||||||
|
assertions = [ ];
|
||||||
|
# Concatenates hostnames from both instances
|
||||||
|
hostname = "bar+johnny foo+johnny";
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user