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:
@@ -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
|
||||||
|
|||||||
@@ -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, ... }:
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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; };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 { };
|
||||||
|
|||||||
@@ -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; })
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
];
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user