feat: custom evaluation #1
5 changed files with 147 additions and 12 deletions
|
@ -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,
|
||||||
|
|
||||||
|
|
|
@ -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,17 +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
|
||||||
colmenaModules.keyChownModule
|
|
||||||
colmenaModules.keyServiceModule
|
|
||||||
colmenaOptions.deploymentOptions
|
colmenaOptions.deploymentOptions
|
||||||
hive.defaults
|
(hive.registry.${hive.${name}.deployment.systemType}.defaults or hive.defaults)
|
||||||
] ++ configs;
|
] ++ configs;
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
@ -179,6 +196,10 @@ let
|
||||||
"allowApplyAll"
|
"allowApplyAll"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
serializableSystemTypeConfigKeys = [
|
||||||
|
"supportsDeployment"
|
||||||
|
];
|
||||||
|
|
||||||
in rec {
|
in rec {
|
||||||
# Exported attributes
|
# Exported attributes
|
||||||
__schema = "v0";
|
__schema = "v0";
|
||||||
|
@ -190,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; };
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,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;
|
||||||
|
@ -87,6 +87,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> {
|
||||||
|
@ -140,6 +142,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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +161,17 @@ impl Hive {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
@ -201,6 +215,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?;
|
||||||
|
@ -234,6 +251,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() {
|
||||||
|
|
|
@ -98,6 +98,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 = mdDoc ''
|
description = mdDoc ''
|
||||||
The target SSH node for deployment.
|
The target SSH node for deployment.
|
||||||
|
@ -218,6 +226,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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue