Merge pull request 'API(cli): add method to Flake class to allow calling nix functions' (#3502) from hsjobeki/clan-core:improvements-2 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3502
This commit is contained in:
hsjobeki
2025-05-05 20:16:28 +00:00
12 changed files with 185 additions and 56 deletions

View File

@@ -21,6 +21,7 @@ in
pkgs, pkgs,
lib, lib,
self', self',
system,
... ...
}: }:
{ {
@@ -83,7 +84,10 @@ in
schema = schema =
(self.clanLib.inventory.evalClanService { (self.clanLib.inventory.evalClanService {
modules = [ m ]; modules = [ m ];
key = "checks"; prefix = [
"checks"
system
];
}).config.result.api.schema; }).config.result.api.schema;
in in
schema schema

View File

@@ -4,7 +4,15 @@
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/hello-word"; manifest.name = "clan-core/hello-word";
roles.peer = { }; roles.peer = {
interface =
{ lib, ... }:
{
options.foo = lib.mkOption {
type = lib.types.str;
};
};
};
perMachine = perMachine =
{ machine, ... }: { machine, ... }:

View File

@@ -50,6 +50,7 @@ in
hello-service = import ./tests/vm/default.nix { hello-service = import ./tests/vm/default.nix {
inherit module; inherit module;
inherit self inputs pkgs; inherit self inputs pkgs;
# clanLib is exposed from inputs.clan-core
clanLib = self.clanLib; clanLib = self.clanLib;
}; };
}; };

View File

@@ -10,6 +10,11 @@ let
in in
{ {
options = { options = {
_prefix = lib.mkOption {
type = types.listOf types.str;
internal = true;
default = [ ];
};
self = lib.mkOption { self = lib.mkOption {
type = types.raw; type = types.raw;
default = self; default = self;
@@ -168,6 +173,7 @@ in
inventoryFile = lib.mkOption { type = lib.types.raw; }; inventoryFile = lib.mkOption { type = lib.types.raw; };
# The machine 'imports' generated by the inventory per machine # The machine 'imports' generated by the inventory per machine
inventoryClass = lib.mkOption { type = lib.types.raw; }; inventoryClass = lib.mkOption { type = lib.types.raw; };
evalServiceSchema = lib.mkOption { };
# clan-core's modules # clan-core's modules
clanModules = lib.mkOption { type = lib.types.raw; }; clanModules = lib.mkOption { type = lib.types.raw; };
source = lib.mkOption { type = lib.types.raw; }; source = lib.mkOption { type = lib.types.raw; };

View File

@@ -44,6 +44,7 @@ let
buildInventory { buildInventory {
inherit inventory directory; inherit inventory directory;
flakeInputs = config.self.inputs; flakeInputs = config.self.inputs;
prefix = config._prefix ++ [ "inventoryClass" ];
} }
); );
@@ -204,6 +205,9 @@ in
inherit inventoryClass; inherit inventoryClass;
# Endpoint that can be called to get a service schema
evalServiceSchema = clan-core.clanLib.evalServiceSchema config.self;
# TODO: unify this interface # TODO: unify this interface
# We should have only clan.modules. (consistent with clan.templates) # We should have only clan.modules. (consistent with clan.templates)
inherit (clan-core) clanModules clanLib; inherit (clan-core) clanModules clanLib;

View File

@@ -15,10 +15,27 @@ lib.fix (clanLib: {
*/ */
callLib = file: args: import file ({ inherit lib clanLib; } // args); callLib = file: args: import file ({ inherit lib clanLib; } // args);
# ------------------------------------
buildClan = clanLib.buildClanModule.buildClanWith { buildClan = clanLib.buildClanModule.buildClanWith {
clan-core = self; clan-core = self;
inherit nixpkgs nix-darwin; inherit nixpkgs nix-darwin;
}; };
evalServiceSchema =
self:
{
moduleSpec,
flakeInputs ? self.inputs,
localModuleSet ? self.clan.modules,
}:
let
resolvedModule = clanLib.inventory.resolveModule {
inherit moduleSpec flakeInputs localModuleSet;
};
in
(clanLib.inventory.evalClanService {
modules = [ resolvedModule ];
prefix = [ ];
}).config.result.api.schema;
# ------------------------------------ # ------------------------------------
# ClanLib functions # ClanLib functions
evalClan = clanLib.callLib ./inventory/eval-clan-modules { }; evalClan = clanLib.callLib ./inventory/eval-clan-modules { };

View File

@@ -12,8 +12,10 @@ let
inventory, inventory,
directory, directory,
flakeInputs, flakeInputs,
prefix ? [ ],
}: }:
(lib.evalModules { (lib.evalModules {
# TODO: remove clanLib from specialArgs
specialArgs = { specialArgs = {
inherit clanLib; inherit clanLib;
}; };
@@ -24,13 +26,16 @@ let
# config.distributedServices.allMachines.${name} or [ ]; # config.distributedServices.allMachines.${name} or [ ];
{ config, ... }: { config, ... }:
{ {
distributedServices = clanLib.inventory.mapInstances { distributedServices = clanLib.inventory.mapInstances {
inherit (config) inventory; inherit (config) inventory;
inherit flakeInputs; inherit flakeInputs;
prefix = prefix ++ [ "distributedServices" ];
}; };
machines = lib.mapAttrs (_machineName: v: { machines = lib.mapAttrs (_machineName: v: {
machineImports = v; machineImports = v;
}) config.distributedServices.allMachines; }) config.distributedServices.allMachines;
} }
) )
(lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; }) (lib.modules.importApply ./inventory-introspection.nix { inherit clanLib; })

View File

@@ -3,7 +3,7 @@ let
services = clanLib.callLib ./distributed-service/inventory-adapter.nix { }; services = clanLib.callLib ./distributed-service/inventory-adapter.nix { };
in in
{ {
inherit (services) evalClanService mapInstances; inherit (services) evalClanService mapInstances resolveModule;
inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory; inherit (import ./build-inventory { inherit lib clanLib; }) buildInventory;
interface = ./build-inventory/interface.nix; interface = ./build-inventory/interface.nix;
# Returns the list of machine names # Returns the list of machine names

View File

@@ -1,7 +1,7 @@
# This module enables itself if # This module enables itself if
# manifest.features.API = true # manifest.features.API = true
# It converts the roles.interface to a json-schema # It converts the roles.interface to a json-schema
{ clanLib, attrName }: { clanLib, prefix }:
let let
converter = clanLib.jsonschema { converter = clanLib.jsonschema {
includeDefaults = true; includeDefaults = true;
@@ -45,7 +45,7 @@ in
To see the evaluation problem run To see the evaluation problem run
nix eval .#clanInternals.inventoryClass.distributedServices.importedModulesEvaluated.${attrName}.config.result.api.schema.${roleName} nix eval .#${lib.concatStringsSep "." prefix}.config.result.api.schema.${roleName}
''; '';
assertion = (builtins.tryEval (lib.deepSeq config.result.api.schema.${roleName} true)).success; assertion = (builtins.tryEval (lib.deepSeq config.result.api.schema.${roleName} true)).success;
}; };

View File

@@ -16,27 +16,72 @@
}: }:
let let
evalClanService = evalClanService =
{ modules, key }: { modules, prefix }:
(lib.evalModules { (lib.evalModules {
class = "clan.service"; class = "clan.service";
modules = [ modules = [
./service-module.nix ./service-module.nix
# feature modules # feature modules
(lib.modules.importApply ./api-feature.nix { (lib.modules.importApply ./api-feature.nix {
inherit clanLib; inherit clanLib prefix;
attrName = key;
}) })
] ++ modules; ] ++ modules;
}); });
resolveModule =
{
moduleSpec,
flakeInputs,
localModuleSet,
}:
let
# TODO:
resolvedModuleSet =
# If the module.name is self then take the modules defined in the flake
# Otherwise its an external input which provides the modules via 'clan.modules' attribute
if moduleSpec.input == null then
localModuleSet
else
let
input =
flakeInputs.${moduleSpec.input} or (throw ''
Flake doesn't provide input with name '${moduleSpec.input}'
Choose one of the following inputs:
- ${
builtins.concatStringsSep "\n- " (
lib.attrNames (lib.filterAttrs (_name: input: input ? clan) flakeInputs)
)
}
To import a local module from 'inventory.modules' remove the 'input' attribute from the module definition
Remove the following line from the module definition:
...
- module.input = "${moduleSpec.input}"
'');
clanAttrs =
input.clan
or (throw "It seems the flake input ${moduleSpec.input} doesn't export any clan resources");
in
clanAttrs.modules;
resolvedModule =
resolvedModuleSet.${moduleSpec.name}
or (throw "flake doesn't provide clan-module with name ${moduleSpec.name}");
in
resolvedModule;
in in
{ {
inherit evalClanService; inherit evalClanService resolveModule;
mapInstances = mapInstances =
{ {
# This is used to resolve the module imports from 'flake.inputs' # This is used to resolve the module imports from 'flake.inputs'
flakeInputs, flakeInputs,
# The clan inventory # The clan inventory
inventory, inventory,
prefix ? [ ],
}: }:
let let
# machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags; # machineHasTag = machineName: tagName: lib.elem tagName inventory.machines.${machineName}.tags;
@@ -45,42 +90,11 @@ in
importedModuleWithInstances = lib.mapAttrs ( importedModuleWithInstances = lib.mapAttrs (
instanceName: instance: instanceName: instance:
let let
# TODO: resolvedModule = resolveModule {
resolvedModuleSet = moduleSpec = instance.module;
# If the module.name is self then take the modules defined in the flake localModuleSet = inventory.modules;
# Otherwise its an external input which provides the modules via 'clan.modules' attribute inherit flakeInputs;
if instance.module.input == null then };
inventory.modules
else
let
input =
flakeInputs.${instance.module.input} or (throw ''
Flake doesn't provide input with name '${instance.module.input}'
Choose one of the following inputs:
- ${
builtins.concatStringsSep "\n- " (
lib.attrNames (lib.filterAttrs (_name: input: input ? clan) flakeInputs)
)
}
To import a local module from 'inventory.modules' remove the 'input' attribute from the module definition
Remove the following line from the module definition:
...
- module.input = "${instance.module.input}"
'');
clanAttrs =
input.clan
or (throw "It seems the flake input ${instance.module.input} doesn't export any clan resources");
in
clanAttrs.modules;
resolvedModule =
resolvedModuleSet.${instance.module.name}
or (throw "flake doesn't provide clan-module with name ${instance.module.name}");
# Every instance includes machines via roles # Every instance includes machines via roles
# :: { client :: ... } # :: { client :: ... }
@@ -138,7 +152,7 @@ in
importedModulesEvaluated = lib.mapAttrs ( importedModulesEvaluated = lib.mapAttrs (
module_ident: instances: module_ident: instances:
evalClanService { evalClanService {
key = module_ident; prefix = prefix ++ [ module_ident ];
modules = modules =
[ [
# Import the resolved module. # Import the resolved module.

View File

@@ -22,6 +22,9 @@ in
pkgs, pkgs,
self, self,
useContainers ? true, useContainers ? true,
# Displayed for better error messages, otherwise the placeholder
system ? "<system>",
attrName ? "<check_name>",
... ...
}: }:
let let
@@ -60,6 +63,15 @@ in
}; };
modules = [ modules = [
clanLib.buildClanModule.flakePartsModule clanLib.buildClanModule.flakePartsModule
{
_prefix = [
"checks"
system
attrName
"config"
"clan"
];
}
]; ];
}; };
}; };

View File

@@ -14,6 +14,7 @@ from clan_cli.nix import (
nix_build, nix_build,
nix_command, nix_command,
nix_config, nix_config,
nix_eval,
nix_metadata, nix_metadata,
nix_test_store, nix_test_store,
) )
@@ -619,11 +620,9 @@ class Flake:
except Exception as e: except Exception as e:
log.warning(f"Failed load eval cache: {e}. Continue without cache") log.warning(f"Failed load eval cache: {e}. Continue without cache")
def invalidate_cache(self) -> None: def prefetch(self) -> None:
""" """
Invalidate the cache and reload it. Loads the flake into the store and populates self.store_path and self.hash such that the flake can evaluate locally and offline
This method is used to refresh the cache by reloading it from the flake.
""" """
cmd = [ cmd = [
"flake", "flake",
@@ -642,6 +641,15 @@ class Flake:
flake_metadata = json.loads(flake_prefetch.stdout) flake_metadata = json.loads(flake_prefetch.stdout)
self.store_path = flake_metadata["storePath"] self.store_path = flake_metadata["storePath"]
self.hash = flake_metadata["hash"] self.hash = flake_metadata["hash"]
self.flake_metadata = flake_metadata
def invalidate_cache(self) -> None:
"""
Invalidate the cache and reload it.
This method is used to refresh the cache by reloading it from the flake.
"""
self.prefetch()
self._cache = FlakeCache() self._cache = FlakeCache()
assert self.hash is not None assert self.hash is not None
@@ -651,17 +659,17 @@ class Flake:
) )
self.load_cache() self.load_cache()
if "original" not in flake_metadata: if "original" not in self.flake_metadata:
flake_metadata = nix_metadata(self.identifier) self.flake_metadata = nix_metadata(self.identifier)
if flake_metadata["original"].get("url", "").startswith("file:"): if self.flake_metadata["original"].get("url", "").startswith("file:"):
self._is_local = True self._is_local = True
path = flake_metadata["original"]["url"].removeprefix("file://") path = self.flake_metadata["original"]["url"].removeprefix("file://")
path = path.removeprefix("file:") path = path.removeprefix("file:")
self._path = Path(path) self._path = Path(path)
elif flake_metadata["original"].get("path"): elif self.flake_metadata["original"].get("path"):
self._is_local = True self._is_local = True
self._path = Path(flake_metadata["original"]["path"]) self._path = Path(self.flake_metadata["original"]["path"])
else: else:
self._is_local = False self._is_local = False
assert self.store_path is not None assert self.store_path is not None
@@ -755,6 +763,56 @@ class Flake:
if self.flake_cache_path: if self.flake_cache_path:
self._cache.save_to_file(self.flake_cache_path) self._cache.save_to_file(self.flake_cache_path)
def uncached_nix_eval_with_args(
self,
attr_path: str,
f_args: dict[str, str],
nix_options: list[str] | None = None,
) -> str:
"""
Calls a nix function with the provided arguments 'f_args'
The argument must be an attribute set.
Args:
attr_path (str): The attribute path to the nix function
f_args (dict[str, nix_expr]): A python dictionary mapping from the name of the argument to a raw nix expression.
Example
flake.uncached_nix_eval_with_args(
"clanInternals.evalServiceSchema",
{ "moduleSpec": "{ name = \"hello-world\"; input = null; }" }
)
> '{ ...JSONSchema... }'
"""
# Always prefetch, so we don't get any stale information
self.prefetch()
if nix_options is None:
nix_options = []
arg_expr = "{"
for arg_name, arg_value in f_args.items():
arg_expr += f" {arg_name} = {arg_value}; "
arg_expr += "}"
nix_code = f"""
let
flake = builtins.getFlake "path:{self.store_path}?narHash={self.hash}";
in
flake.{attr_path} {arg_expr}
"""
if tmp_store := nix_test_store():
nix_options += ["--store", str(tmp_store)]
nix_options.append("--impure")
output = run(
nix_eval(["--expr", nix_code, *nix_options]), RunOpts(log=Log.NONE)
).stdout.strip()
return output
def precache( def precache(
self, self,
selectors: list[str], selectors: list[str],