Merge pull request 'chore(lib/select): move into subfolder with a test file' (#3175) from hsjobeki/clan-core:lib-cleanup into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3175
This commit is contained in:
hsjobeki
2025-04-04 12:32:32 +00:00
19 changed files with 558 additions and 54 deletions

View File

@@ -39,4 +39,45 @@
acc ++ tagMembers
) [ ] members.tags or [ ]);
};
/**
Checks whether a module has a specific class
# Arguments
- `module` The module to check.
# Returns
- `string` | null: The specified class, or null if the class is not set
# Throws
- If the module is not a valid module
- If the module has a type that is not supported
*/
getModuleClass =
module:
let
loadModuleForClassCheck =
m:
# Logic path adapted from nixpkgs/lib/modules.nix
if lib.isFunction m then
let
args = lib.functionArgs m;
in
m args
else if lib.isAttrs m then
# module doesn't have a _type attribute
if m._type or "module" == "module" then
m
# module has a _type set but it is not "module"
else if m._type == "if" || m._type == "override" then
throw "Module modifiers are not supported yet. Got: ${m._type}"
else
throw "Unsupported module type ${lib.typeOf m}"
else if lib.isList m then
throw "Invalid or unsupported module type ${lib.typeOf m}"
else
import m;
loaded = loadModuleForClassCheck module;
in
if loaded ? _class then loaded._class else null;
}

View File

@@ -63,6 +63,7 @@ let
resolvedModule =
resolvedModuleSet.${instance.module.name}
or (throw "flake doesn't provide clan-module with name ${instance.module.name}");
moduleClass = clanLib.inventory.getModuleClass resolvedModule;
# Every instance includes machines via roles
# :: { client :: ... }
@@ -86,13 +87,13 @@ let
machineName:
let
machineSettings = instance.roles.${roleName}.machines.${machineName}.settings or { };
# TODO: tag settings
# Wait for this feature until option introspection for 'settings' is done.
# This might get too complex to handle otherwise.
# settingsViaTags = lib.filterAttrs (
# tagName: _: machineHasTag machineName tagName
# ) instance.roles.${roleName}.tags;
in
# TODO: tag settings
# Wait for this feature until option introspection for 'settings' is done.
# This might get too complex to handle otherwise.
# settingsViaTags = lib.filterAttrs (
# tagName: _: machineHasTag machineName tagName
# ) instance.roles.${roleName}.tags;
{
# TODO: Do we want to wrap settings with
# setDefaultModuleLocation "inventory.instances.${instanceName}.roles.${roleName}.tags.${tagName}";
@@ -112,20 +113,29 @@ let
in
{
inherit (instance) module;
inherit resolvedModule instanceRoles;
inherit resolvedModule instanceRoles moduleClass;
}
) inventory.instances;
# TODO: Eagerly check the _class of the resolved module
importedModulesEvaluated = lib.mapAttrs (
_module_ident: instances:
let
matchedClass = "clan.service";
instance = (builtins.head instances).instance;
classCheckedModule =
if instance.moduleClass == matchedClass then
instance.resolvedModule
else
(throw ''Module '${instance.module.name}' is not a valid '${matchedClass}' module. Got module with class:${builtins.toJSON instance.moduleClass}'');
in
(lib.evalModules {
class = "clan.service";
class = matchedClass;
modules =
[
./service-module.nix
# Import the resolved module
(builtins.head instances).instance.resolvedModule
classCheckedModule
]
# Include all the instances that correlate to the resolved module
++ (builtins.map (v: {

View File

@@ -317,20 +317,24 @@ in
*/
v: instanceName: machineName:
(lib.evalModules {
specialArgs = {
inherit instanceName;
machine = {
name = machineName;
specialArgs =
let
roles = applySettings instanceName config.instances.${instanceName};
in
{
inherit instanceName roles;
machine = {
name = machineName;
roles = lib.attrNames (lib.filterAttrs (_n: v: v.machines ? ${machineName}) roles);
};
settings = (
makeExtensibleConfig evalMachineSettings {
inherit roleName instanceName machineName;
settings =
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.settings or { };
}
);
};
settings = (
makeExtensibleConfig evalMachineSettings {
inherit roleName instanceName machineName;
settings =
config.instances.${instanceName}.roles.${roleName}.machines.${machineName}.settings or { };
}
);
};
modules = [ v ];
}).config;
};

View File

@@ -22,6 +22,15 @@ let
}).config;
flakeInputsFixture = {
# Example upstream module
upstream.clan.modules = {
uzzi = {
_class = "clan.service";
manifest = {
name = "uzzi-from-upstream";
};
};
};
};
callInventoryAdapter =
@@ -32,6 +41,7 @@ let
};
in
{
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
test_simple =
let
res = callInventoryAdapter {
@@ -57,6 +67,7 @@ in
# We might change the attribute name in the future
expr = res.importedModulesEvaluated ? "self-simple-module";
expected = true;
inherit res;
};
# A module can be imported multiple times

View File

@@ -0,0 +1,65 @@
{ callInventoryAdapter, ... }:
let
# Authored module
# A minimal module looks like this
# It isn't exactly doing anything but it's a valid module that produces an output
modules."A" = {
_class = "clan.service";
manifest = {
name = "network";
};
};
modules."B" =
{ ... }:
{
options.stuff = "legacy-clan-service";
};
machines = {
jon = { };
sara = { };
};
resolve =
spec:
callInventoryAdapter {
inherit modules machines;
instances."instance_foo" = {
module = spec;
};
};
in
{
test_import_local_module_by_name = {
expr = (resolve { name = "A"; }).importedModuleWithInstances.instance_foo.resolvedModule;
expected = {
_class = "clan.service";
manifest = {
name = "network";
};
};
};
test_import_remote_module_by_name = {
expr =
(resolve {
name = "uzzi";
input = "upstream";
}).importedModuleWithInstances.instance_foo.resolvedModule;
expected = {
_class = "clan.service";
manifest = {
name = "uzzi-from-upstream";
};
};
};
# Currently this should fail
# TODO: Can we implement a default wrapper to make migration easy?
test_import_local_legacy_module = {
expr = (resolve { name = "B"; }).allMachines;
expectedError = {
type = "ThrownError";
msg = "Module 'B' is not a valid 'clan.service' module.*";
};
};
}

View File

@@ -10,6 +10,7 @@ let
};
# Define two roles with unmergeable interfaces
# Both define some 'timeout' but with completely different types.
roles.controller = { };
roles.peer.interface =
{ lib, ... }:
{
@@ -23,6 +24,7 @@ let
instanceName,
settings,
machine,
roles,
...
}:
let
@@ -35,7 +37,12 @@ let
in
{
nixosModule = {
inherit instanceName settings machine;
inherit
instanceName
settings
machine
roles
;
# We are double vendoring the settings
# To test that we can do it indefinitely
@@ -64,6 +71,7 @@ let
roles.peer = {
settings.timeout = "foo-peer";
};
roles.controller.machines.jon = { };
};
instances."instance_bar" = {
module = {
@@ -73,6 +81,8 @@ let
settings.timeout = "bar-peer-jon";
};
};
# TODO: move this into a seperate test.
# Seperate out the check that this module is never imported
# import the module "B" (undefined)
# All machines have this instance
instances."instance_zaza" = {
@@ -108,17 +118,9 @@ in
# roles = peer
# machines = jon
settings = filterInternals res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.nixosModule.settings;
machine = mapInternalsRecursive res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.nixosModule.machine;
# hasRoleSettings =
# res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer ? settings;
# # settings are specific.
# # Below we access:
# # instance = instance_foo
# # roles = peer
# # machines = *
# specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer.settings;
machine =
res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.nixosModule.machine;
roles = mapInternalsRecursive res.importedModulesEvaluated.self-A.config.result.allRoles.peer.allInstances.instance_foo.allMachines.jon.nixosModule.roles;
};
expected = {
instanceName = "instance_foo";
@@ -127,21 +129,37 @@ in
};
machine = {
name = "jon";
roles = {
peer = {
machines = {
jon = {
settings = {
__functor = "__functor";
timeout = "foo-peer-jon";
};
roles = [
"controller"
"peer"
];
};
roles = {
controller = {
machines = {
jon = {
settings = {
__functor = "__functor";
};
};
settings = {
__functor = "__functor";
timeout = "foo-peer";
};
settings = {
__functor = "__functor";
};
};
peer = {
machines = {
jon = {
settings = {
__functor = "__functor";
timeout = "foo-peer-jon";
};
};
};
settings = {
__functor = "__functor";
timeout = "foo-peer";
};
};
};
};

View File

@@ -26,9 +26,11 @@ let
};
perMachine =
{ instances, ... }:
{ instances, machine, ... }:
{
nixosModule = instances;
nixosModule = {
inherit instances machine;
};
};
};
machines = {
@@ -71,9 +73,10 @@ in
# settings should evaluate
test_per_machine_receives_instance_settings = {
inherit res;
expr = {
hasMachineSettings =
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer.machines.jon
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon
? settings;
# settings are specific.
@@ -81,10 +84,10 @@ in
# instance = instance_foo
# roles = peer
# machines = jon
specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer.machines.jon.settings;
specificMachineSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.machines.jon.settings;
hasRoleSettings =
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer
res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer
? settings;
# settings are specific.
@@ -92,7 +95,7 @@ in
# instance = instance_foo
# roles = peer
# machines = *
specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instance_foo.roles.peer.settings;
specificRoleSettings = filterInternals res.importedModulesEvaluated.self-A.config.result.allMachines.jon.nixosModule.instances.instance_foo.roles.peer.settings;
};
expected = {
hasMachineSettings = true;