feat: generic registry and custom evaluation

This PR bring custom evaluation, but does not offer yet custom
activation.

Therefore, you can evaluate your systems and refer to each of them, but
you cannot ask Colmena to build them for you.

Signed-off-by: Ryan Lahfa <ryan@dgnum.eu>
This commit is contained in:
Ryan Lahfa 2024-05-24 20:55:22 +02:00 committed by Tom Hubrecht
parent 7fa3062cfb
commit 92c5f5c33f
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
5 changed files with 147 additions and 10 deletions

View file

@ -64,6 +64,12 @@ pub enum ColmenaError {
#[snafu(display("Don't know how to connect to the node"))] #[snafu(display("Don't know how to connect to the node"))]
NoTargetHost, NoTargetHost,
#[snafu(display(
"Don't know how to deploy node: {} -- does your system type support deployment?",
node_name
))]
UndeployableHost { node_name: String },
#[snafu(display("Node name cannot be empty"))] #[snafu(display("Node name cannot be empty"))]
EmptyNodeName, EmptyNodeName,

View file

@ -38,19 +38,25 @@ let
else if uncheckedHive ? network then uncheckedHive.network else if uncheckedHive ? network then uncheckedHive.network
else {}; else {};
uncheckedRegistries = if uncheckedHive ? registry then uncheckedHive.registry else {};
# The final hive will always have the meta key instead of network. # The final hive will always have the meta key instead of network.
hive = let hive = let
userMeta = (lib.modules.evalModules { userMeta = (lib.modules.evalModules {
modules = [ colmenaOptions.metaOptions uncheckedUserMeta ]; modules = [ colmenaOptions.metaOptions uncheckedUserMeta ];
}).config; }).config;
registry = (lib.modules.evalModules {
modules = [ colmenaOptions.registryOptions { registry = uncheckedRegistries; } ];
}).config.registry;
mergedHive = mergedHive =
assert lib.assertMsg (!(uncheckedHive ? __schema)) '' assert lib.assertMsg (!(uncheckedHive ? __schema)) ''
You cannot pass in an already-evaluated Hive into the evaluator. You cannot pass in an already-evaluated Hive into the evaluator.
Hint: Use the `colmenaHive` output instead of `colmena`. Hint: Use the `colmenaHive` output instead of `colmena`.
''; '';
removeAttrs (defaultHive // uncheckedHive) [ "meta" "network" ]; removeAttrs (defaultHive // uncheckedHive) [ "meta" "network" "registry" ];
meta = { meta = {
meta = meta =
@ -58,7 +64,7 @@ let
then userMeta // { nixpkgs = <nixpkgs>; } then userMeta // { nixpkgs = <nixpkgs>; }
else userMeta; else userMeta;
}; };
in mergedHive // meta; in mergedHive // meta // { inherit registry; };
configsFor = node: let configsFor = node: let
nodeConfig = hive.${node}; nodeConfig = hive.${node};
@ -112,14 +118,23 @@ let
in mkNixpkgs "meta.nixpkgs" nixpkgsConf; in mkNixpkgs "meta.nixpkgs" nixpkgsConf;
lib = nixpkgs.lib; lib = nixpkgs.lib;
reservedNames = [ "defaults" "network" "meta" ]; reservedNames = [ "defaults" "network" "meta" "registry" ];
evalNode = name: configs: let evalNode = name: configs:
# Some help on error messages.
assert (lib.assertMsg (lib.hasAttrByPath [ "deployment" "systemType" ] hive.${name})
"${name} does not have a deployment system type!");
assert (lib.assertMsg (builtins.typeOf hive.registry == "set"))
"The hive's registry is not a set, but of type '${builtins.typeOf hive.registry}'";
assert (lib.assertMsg (lib.hasAttr hive.${name}.deployment.systemType hive.registry)
"${builtins.toJSON (hive.${name}.deployment.systemType)} does not exist in the registry of systems!");
let
# We cannot use `configs` because we need to access to the raw configuration fragment.
inherit (hive.registry.${hive.${name}.deployment.systemType}) evalConfig;
npkgs = npkgs =
if hasAttr name hive.meta.nodeNixpkgs if hasAttr name hive.meta.nodeNixpkgs
then mkNixpkgs "meta.nodeNixpkgs.${name}" hive.meta.nodeNixpkgs.${name} then mkNixpkgs "meta.nodeNixpkgs.${name}" hive.meta.nodeNixpkgs.${name}
else nixpkgs; else nixpkgs;
evalConfig = import (npkgs.path + "/nixos/lib/eval-config.nix");
# Here we need to merge the configurations in meta.nixpkgs # Here we need to merge the configurations in meta.nixpkgs
# and in machine config. # and in machine config.
@ -139,15 +154,19 @@ let
in in
lib.optional (!hasTypedConfig && length remainingKeys != 0) lib.optional (!hasTypedConfig && length remainingKeys != 0)
"The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}"; "The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}";
}; } // lib.optionalAttrs (builtins.hasAttr "localSystem" npkgs || builtins.hasAttr "crossSystem" npkgs) {
nixpkgs.localSystem = lib.mkBefore npkgs.localSystem;
nixpkgs.crossSystem = lib.mkBefore npkgs.crossSystem;
};
in evalConfig { in evalConfig {
inherit (npkgs) system; # This doesn't exist for `evalModules` the generic way.
# inherit (npkgs) system;
modules = [ modules = [
nixpkgsModule nixpkgsModule
colmenaModules.assertionModule colmenaModules.assertionModule
colmenaOptions.deploymentOptions colmenaOptions.deploymentOptions
hive.defaults (hive.registry.${hive.${name}.deployment.systemType}.defaults or hive.defaults)
] ++ configs; ] ++ configs;
specialArgs = { specialArgs = {
inherit name; inherit name;
@ -177,6 +196,10 @@ let
"allowApplyAll" "allowApplyAll"
]; ];
serializableSystemTypeConfigKeys = [
"supportsDeployment"
];
in rec { in rec {
# Exported attributes # Exported attributes
__schema = "v0.20241006"; __schema = "v0.20241006";
@ -188,5 +211,9 @@ in rec {
evalSelected = names: lib.filterAttrs (name: _: elem name names) toplevel; evalSelected = names: lib.filterAttrs (name: _: elem name names) toplevel;
evalSelectedDrvPaths = names: lib.mapAttrs (_: v: v.drvPath) (evalSelected names); evalSelectedDrvPaths = names: lib.mapAttrs (_: v: v.drvPath) (evalSelected names);
metaConfig = lib.filterAttrs (n: v: elem n metaConfigKeys) hive.meta; metaConfig = lib.filterAttrs (n: v: elem n metaConfigKeys) hive.meta;
introspect = f: f { inherit lib; pkgs = nixpkgs; nodes = uncheckedNodes; }; # We cannot perform a `metaConfigKeys`-style simple check here
# because registry is arbitrarily deep and may evaluate nixpkgs indirectly.
registryConfig = lib.mapAttrs (systemTypeName: systemType:
lib.filterAttrs (n: v: elem n serializableSystemTypeConfigKeys) systemType) hive.registry;
introspect = f: f { inherit lib; pkgs = nixpkgs; inherit nodes; };
} }

View file

@ -16,7 +16,7 @@ use validator::Validate;
use super::deployment::TargetNode; use super::deployment::TargetNode;
use super::{ use super::{
Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName, Flake, MetaConfig, NixExpression, NixFlags, NodeConfig, NodeFilter, NodeName,
ProfileDerivation, SerializedNixExpression, StorePath, ProfileDerivation, RegistryConfig, SerializedNixExpression, StorePath,
}; };
use crate::error::{ColmenaError, ColmenaResult}; use crate::error::{ColmenaError, ColmenaResult};
use crate::job::JobHandle; use crate::job::JobHandle;
@ -125,6 +125,8 @@ pub struct Hive {
nix_options: HashMap<String, String>, nix_options: HashMap<String, String>,
meta_config: OnceCell<MetaConfig>, meta_config: OnceCell<MetaConfig>,
registry_config: OnceCell<RegistryConfig>,
} }
struct NixInstantiate<'hive> { struct NixInstantiate<'hive> {
@ -179,6 +181,7 @@ impl Hive {
impure: false, impure: false,
nix_options: HashMap::new(), nix_options: HashMap::new(),
meta_config: OnceCell::new(), meta_config: OnceCell::new(),
registry_config: OnceCell::new(),
}) })
} }
@ -205,6 +208,17 @@ impl Hive {
self.evaluation_method = method; self.evaluation_method = method;
} }
pub async fn get_registry_config(&self) -> ColmenaResult<&RegistryConfig> {
self.registry_config
.get_or_try_init(|| async {
self.nix_instantiate("hive.registryConfig")
.eval()
.capture_json()
.await
})
.await
}
pub fn set_show_trace(&mut self, value: bool) { pub fn set_show_trace(&mut self, value: bool) {
self.show_trace = value; self.show_trace = value;
} }
@ -248,6 +262,9 @@ impl Hive {
) -> ColmenaResult<HashMap<NodeName, TargetNode>> { ) -> ColmenaResult<HashMap<NodeName, TargetNode>> {
let mut node_configs = None; let mut node_configs = None;
log::info!("Enumerating systems...");
let registry = self.get_registry_config().await?;
log::info!("Enumerating nodes..."); log::info!("Enumerating nodes...");
let all_nodes = self.node_names().await?; let all_nodes = self.node_names().await?;
@ -281,6 +298,24 @@ impl Hive {
self.deployment_info_selected(&selected_nodes).await? self.deployment_info_selected(&selected_nodes).await?
}; };
for node_config in &node_configs {
if let Some(system_type) = node_config.1.system_type.as_ref() {
let Some(system_config) = registry.systems.get(system_type) else {
// TODO: convert me to proper error?
log::warn!("'{:?}' is not a known system type in the registry, double check your expressions!", system_type);
return Err(ColmenaError::Unknown {
message: "unknown system type".to_string(),
});
};
if !system_config.supports_deployment {
return Err(ColmenaError::UndeployableHost {
node_name: node_config.0.to_string(),
});
}
}
}
let mut targets = HashMap::new(); let mut targets = HashMap::new();
let mut n_ssh = 0; let mut n_ssh = 0;
for node in selected_nodes.into_iter() { for node in selected_nodes.into_iter() {

View file

@ -96,6 +96,14 @@ with builtins; rec {
in { in {
options = { options = {
deployment = { deployment = {
systemType = lib.mkOption {
description = mdDoc ''
System type used for this node, e.g. NixOS.
'';
default = "nixos";
# TODO: enum among all registered systems?
type = types.str;
};
targetHost = lib.mkOption { targetHost = lib.mkOption {
description = '' description = ''
The target SSH node for deployment. The target SSH node for deployment.
@ -216,6 +224,52 @@ with builtins; rec {
}; };
}; };
}; };
# Options for a registered system type
systemTypeOptions = { name, lib, ... }: let
inherit (lib) types;
mdDoc = lib.mdDoc or lib.id;
in
{
options = {
evalConfig = lib.mkOption {
description = mdDoc ''
Evaluation function which share the same interface as `nixos/lib/eval-config.nix`
which can be tailored to your own usecases or to target another type of system,
e.g. nix-darwin.
'';
type = types.functionTo types.unspecified;
};
supportsDeployment = lib.mkOption {
description = mdDoc ''
Whether this system type supports deployment or not.
If it supports deployment, it needs to have appropriate activation code,
refer to how to write custom activators.
'';
default = name == "nixos";
defaultText = "If a NixOS system, then true, otherwise false by default";
};
defaults = lib.mkOption {
description = mdDoc ''
Default configuration for that system type.
'';
type = types.functionTo types.unspecified;
default = _: {};
};
};
};
registryOptions = { lib, ... }: let
inherit (lib) types;
mdDoc = lib.mdDoc or lib.id;
in
{
options.registry = lib.mkOption {
description = mdDoc ''
A registry of all system types.
'';
type = types.attrsOf (types.submodule systemTypeOptions);
};
};
# Hive-wide options # Hive-wide options
metaOptions = { lib, ... }: let metaOptions = { lib, ... }: let
inherit (lib) types; inherit (lib) types;

View file

@ -55,6 +55,9 @@ pub struct NodeName(#[serde(deserialize_with = "NodeName::deserialize")] String)
#[derive(Debug, Clone, Validate, Deserialize)] #[derive(Debug, Clone, Validate, Deserialize)]
pub struct NodeConfig { pub struct NodeConfig {
#[serde(rename = "systemType")]
system_type: Option<String>,
#[serde(rename = "targetHost")] #[serde(rename = "targetHost")]
target_host: Option<String>, target_host: Option<String>,
@ -94,6 +97,18 @@ pub struct MetaConfig {
pub machines_file: Option<String>, pub machines_file: Option<String>,
} }
#[derive(Debug, Clone, Validate, Deserialize)]
pub struct SystemTypeConfig {
#[serde(rename = "supportsDeployment")]
pub supports_deployment: bool,
}
#[derive(Debug, Clone, Validate, Deserialize)]
pub struct RegistryConfig {
#[serde(flatten)]
pub systems: HashMap<String, SystemTypeConfig>,
}
/// Nix CLI flags. /// Nix CLI flags.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct NixFlags { pub struct NixFlags {