chore: Split options and modules into multiple files

This commit is contained in:
Tom Hubrecht 2025-02-20 14:29:29 +01:00
parent 508da85384
commit a2a58ceda9
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
12 changed files with 628 additions and 574 deletions

View file

@ -35,8 +35,6 @@
"x86_64-darwin"
"aarch64-darwin"
];
colmenaOptions = import ./src/nix/hive/options.nix;
colmenaModules = import ./src/nix/hive/modules.nix;
in
flake-utils.lib.eachSystem supportedSystems (
system:
@ -71,7 +69,7 @@
inherit
(pkgs.lib.evalModules {
modules = [
colmenaOptions.deploymentOptions
./src/nix/hive/options/deployment.nix
suppressModuleArgsDocs
];
specialArgs = {
@ -87,7 +85,7 @@
inherit
(pkgs.lib.evalModules {
modules = [
colmenaOptions.metaOptions
./src/nix/hive/options/meta.nix
suppressModuleArgsDocs
];
})
@ -166,15 +164,18 @@
colmena = final.callPackage ./package.nix { };
};
nixosModules = {
inherit (colmenaOptions) deploymentOptions metaOptions;
inherit (colmenaModules) keyChownModule keyServiceModule assertionModule;
deploymentOptions = import ./src/nix/hive/options/deployment.nix;
metaOptions = import ./src/nix/hive/options/meta.nix;
keyChownModule = import ./src/nix/hive/modules/key-chown.nix;
keyServiceModule = import ./src/nix/hive/modules/key-service.nix;
assertionModule = import ./src/nix/hive/modules/assertions.nix;
};
lib.makeHive =
rawHive:
import ./src/nix/hive/eval.nix {
inherit rawHive colmenaOptions colmenaModules;
inherit rawHive;
hermetic = true;
};

View file

@ -2,10 +2,7 @@
rawHive ? null, # Colmena Hive attrset
rawFlake ? null, # Nix Flake attrset with `outputs.colmena`
hermetic ? rawFlake != null, # Whether we are allowed to use <nixpkgs>
colmenaOptions ? import ./options.nix,
colmenaModules ? import ./modules.nix,
}:
with builtins;
let
defaultHive = {
@ -29,9 +26,9 @@ let
rawToHive =
rawHive:
if typeOf rawHive == "lambda" || rawHive ? __functor then
if (builtins.isFunction rawHive) || rawHive ? __functor then
rawHive { }
else if typeOf rawHive == "set" then
else if (builtins.isAttrs rawHive) then
rawHive
else
throw "The config must evaluate to an attribute set.";
@ -61,7 +58,7 @@ let
userMeta =
(lib.modules.evalModules {
modules = [
colmenaOptions.metaOptions
./options/meta.nix
uncheckedUserMeta
];
}).config;
@ -69,7 +66,7 @@ let
registry =
(lib.modules.evalModules {
modules = [
colmenaOptions.registryOptions
./options/registry.nix
{ registry = uncheckedRegistries; }
];
}).config.registry;
@ -99,9 +96,9 @@ let
nodeConfig = hive.${node};
in
assert lib.assertMsg (
!elem node reservedNames
!builtins.elem node reservedNames
) "\"${node}\" is a reserved name and cannot be used as the name of a node";
if typeOf nodeConfig == "list" then nodeConfig else [ nodeConfig ];
if (builtins.isList nodeConfig) then nodeConfig else [ nodeConfig ];
mkNixpkgs =
configName: pkgConf:
@ -123,15 +120,15 @@ let
}
'';
in
if typeOf pkgConf == "path" || (typeOf pkgConf == "set" && pkgConf ? outPath) then
if (builtins.isPath pkgConf) || ((builtins.isAttrs pkgConf) && pkgConf ? outPath) then
if hermetic then
throw (uninitializedError "a path to Nixpkgs")
# The referenced file might return an initialized Nixpkgs attribute set directly
else
mkNixpkgs configName (import pkgConf)
else if typeOf pkgConf == "lambda" then
else if (builtins.isFunction pkgConf) then
if hermetic then throw (uninitializedError "a Nixpkgs lambda") else pkgConf { overlays = [ ]; }
else if typeOf pkgConf == "set" then
else if (builtins.isAttrs pkgConf) then
if pkgConf ? outputs then throw (uninitializedError "an uninitialized Nixpkgs input") else pkgConf
else
throw ''
@ -173,7 +170,7 @@ let
] hive.${name}) "${name} does not have a deployment system type!"
);
assert (lib.assertMsg (
builtins.typeOf hive.registry == "set"
builtins.isAttrs hive.registry
)) "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)
@ -183,7 +180,7 @@ let
# We cannot use `configs` because we need to access to the raw configuration fragment.
inherit (hive.registry.${hive.${name}.deployment.systemType}) evalConfig;
npkgs =
if hasAttr name hive.meta.nodeNixpkgs then
if builtins.hasAttr name hive.meta.nodeNixpkgs then
mkNixpkgs "meta.nodeNixpkgs.${name}" hive.meta.nodeNixpkgs.${name}
else
nixpkgs;
@ -205,15 +202,15 @@ let
# Before 22.11, most config keys were untyped thus the merging
# was broken. Let's warn the user if not all config attributes
# set in meta.nixpkgs are overridden.
metaKeys = attrNames npkgs.config;
metaKeys = builtins.attrNames npkgs.config;
nodeKeys = [
"doCheckByDefault"
"warnings"
"allowAliases"
] ++ (attrNames config.nixpkgs.config);
remainingKeys = filter (k: !elem k nodeKeys) metaKeys;
] ++ (builtins.attrNames config.nixpkgs.config);
remainingKeys = builtins.filter (k: !builtins.elem k nodeKeys) metaKeys;
in
lib.optional (!hasTypedConfig && length remainingKeys != 0)
lib.optional (!hasTypedConfig && builtins.length remainingKeys != 0)
"The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}";
}
//
@ -229,8 +226,8 @@ let
modules = [
nixpkgsModule
colmenaModules.assertionModule
colmenaOptions.deploymentOptions
./modules/assertions.nix
./options/deployment.nix
(hive.registry.${hive.${name}.deployment.systemType}.defaults or hive.defaults)
] ++ configs;
specialArgs =
@ -242,12 +239,12 @@ let
// (hive.meta.nodeSpecialArgs.${name} or { });
};
nodeNames = filter (name: !elem name reservedNames) (attrNames hive);
nodeNames = builtins.filter (name: !builtins.elem name reservedNames) (builtins.attrNames hive);
# Used as the `nodes` argument in modules. We skip recursive type checking
# for performance.
uncheckedNodes = listToAttrs (
map (
uncheckedNodes = builtins.listToAttrs (
builtins.map (
name:
let
configs = [
@ -280,23 +277,24 @@ rec {
# Exported attributes
__schema = "v0.20241006";
nodes = listToAttrs (
map (name: {
nodes = builtins.listToAttrs (
builtins.map (name: {
inherit name;
value = evalNode name (configsFor name);
}) nodeNames
);
toplevel = lib.mapAttrs (_: v: v.config.system.build.toplevel) nodes;
deploymentConfig = lib.mapAttrs (_: v: v.config.deployment) nodes;
deploymentConfigSelected = names: lib.filterAttrs (name: _: elem name names) deploymentConfig;
evalSelected = names: lib.filterAttrs (name: _: elem name names) toplevel;
deploymentConfigSelected =
names: lib.filterAttrs (name: _: builtins.elem name names) deploymentConfig;
evalSelected = names: lib.filterAttrs (name: _: builtins.elem name names) toplevel;
evalSelectedDrvPaths = names: lib.mapAttrs (_: v: v.drvPath) (evalSelected names);
metaConfig = lib.filterAttrs (n: v: elem n metaConfigKeys) hive.meta;
metaConfig = lib.filterAttrs (n: v: builtins.elem n metaConfigKeys) hive.meta;
# 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
lib.filterAttrs (n: v: builtins.elem n serializableSystemTypeConfigKeys) systemType
) hive.registry;
introspect =
f:

View file

@ -6,7 +6,7 @@
};
outputs =
{ self, hive }:
{ hive, ... }:
{
processFlake =
let
@ -20,8 +20,6 @@
import ./eval.nix {
inherit rawFlake;
hermetic = true;
colmenaOptions = import ./options.nix;
colmenaModules = import ./modules.nix;
};
# Uses an already-evaluated hive.

View file

@ -1,139 +0,0 @@
with builtins;
{
assertionModule =
{ config, lib, ... }:
{
assertions = lib.mapAttrsToList (
key: opts:
let
nonNulls = l: filter (x: x != null) l;
in
{
assertion =
length (nonNulls [
opts.text
opts.keyCommand
opts.keyFile
]) == 1;
message =
let
prefix = "${name}.deployment.keys.${key}";
in
"Exactly one of `${prefix}.text`, `${prefix}.keyCommand` and `${prefix}.keyFile` must be set.";
}
) config.deployment.keys;
};
# Change the ownership of all keys uploaded pre-activation
#
# This is built as part of the system profile.
# We must be careful not to access `text` / `keyCommand` / `keyFile` here
keyChownModule =
{ lib, config, ... }:
let
preActivationKeys = lib.filterAttrs (
name: key: key.uploadAt == "pre-activation"
) config.deployment.keys;
scriptDeps = if config.system.activationScripts ? groups then [ "groups" ] else [ "users" ];
commands = lib.mapAttrsToList (
name: key:
let
keyPath = "${key.destDir}/${name}";
in
''
if [ -f "${keyPath}" ]; then
if ! chown ${key.user}:${key.group} "${keyPath}"; then
# Error should be visible in stderr
failed=1
fi
else
>&2 echo "Key ${keyPath} does not exist. Skipping chown."
fi
''
) preActivationKeys;
script = lib.stringAfter scriptDeps ''
# This script is injected by Colmena to change the ownerships
# of keys (`deployment.keys`) deployed before system activation.
>&2 echo "setting up key ownerships..."
# We set the ownership of as many keys as possible before failing
failed=
${concatStringsSep "\n" commands}
if [ -n "$failed" ]; then
>&2 echo "Failed to set the ownership of some keys."
# The activation script has a trap to handle failed
# commands and print out various debug information.
# Let's trigger that instead of `exit 1`.
false
fi
'';
in
{
system.activationScripts.colmena-chown-keys = lib.mkIf (length commands != 0) script;
};
# Create "${name}-key" services for NixOps compatibility
#
# This is built as part of the system profile.
# We must be careful not to access `text` / `keyCommand` / `keyFile` here
#
# Sadly, path units don't automatically deactivate the bound units when
# the key files are deleted, so we use inotifywait in the services' scripts.
#
# <https://github.com/systemd/systemd/issues/3642>
keyServiceModule =
{
pkgs,
lib,
config,
...
}:
{
systemd.paths = lib.mapAttrs' (name: val: {
name = "${name}-key";
value = {
wantedBy = [ "paths.target" ];
pathConfig = {
PathExists = val.path;
};
};
}) config.deployment.keys;
systemd.services = lib.mapAttrs' (name: val: {
name = "${name}-key";
value = {
enable = true;
serviceConfig = {
TimeoutStartSec = "infinity";
Restart = "always";
RestartSec = "100ms";
};
path = [ pkgs.inotify-tools ];
preStart = ''
(while read f; do if [ "$f" = "${val.name}" ]; then break; fi; done \
< <(inotifywait -qm --format '%f' -e create,move ${val.destDir}) ) &
if [[ -e "${val.path}" ]]; then
echo 'flapped down'
kill %1
exit 0
fi
wait %1
'';
script = ''
inotifywait -qq -e delete_self "${val.path}" &
if [[ ! -e "${val.path}" ]]; then
echo 'flapped up'
exit 0
fi
wait %1
'';
};
}) config.deployment.keys;
};
}

View file

@ -0,0 +1,28 @@
{
config,
lib,
name,
...
}:
let
inherit (lib) filter mapAttrsToList;
in
{
assertions = mapAttrsToList (key: opts: {
assertion =
builtins.length (
filter (x: x != null) [
opts.text
opts.keyCommand
opts.keyFile
]
) == 1;
message =
let
prefix = "${name}.deployment.keys.${key}";
in
"Exactly one of `${prefix}.text`, `${prefix}.keyCommand` and `${prefix}.keyFile` must be set.";
}) config.deployment.keys;
}

View file

@ -0,0 +1,64 @@
# Change the ownership of all keys uploaded pre-activation
#
# This is built as part of the system profile.
# We must be careful not to access `text` / `keyCommand` / `keyFile` here
{ config, lib, ... }:
let
inherit (lib)
concatStringsSep
filterAttrs
mapAttrsToList
mkIf
stringAfter
;
preActivationKeys = filterAttrs (
_: { uploadAt, ... }: uploadAt == "pre-activation"
) config.deployment.keys;
scriptDeps = if config.system.activationScripts ? groups then [ "groups" ] else [ "users" ];
commands = mapAttrsToList (
name: key:
let
keyPath = "${key.destDir}/${name}";
in
''
if [ -f "${keyPath}" ]; then
if ! chown ${key.user}:${key.group} "${keyPath}"; then
# Error should be visible in stderr
failed=1
fi
else
>&2 echo "Key ${keyPath} does not exist. Skipping chown."
fi
''
) preActivationKeys;
script = stringAfter scriptDeps ''
# This script is injected by Colmena to change the ownerships
# of keys (`deployment.keys`) deployed before system activation.
>&2 echo "setting up key ownerships..."
# We set the ownership of as many keys as possible before failing
failed=
${concatStringsSep "\n" commands}
if [ -n "$failed" ]; then
>&2 echo "Failed to set the ownership of some keys."
# The activation script has a trap to handle failed
# commands and print out various debug information.
# Let's trigger that instead of `exit 1`.
false
fi
'';
in
{
system.activationScripts.colmena-chown-keys = mkIf (commands != [ ]) script;
}

View file

@ -0,0 +1,61 @@
# Create "${name}-key" services for NixOps compatibility
#
# This is built as part of the system profile.
# We must be careful not to access `text` / `keyCommand` / `keyFile` here
#
# Sadly, path units don't automatically deactivate the bound units when
# the key files are deleted, so we use inotifywait in the services' scripts.
#
# <https://github.com/systemd/systemd/issues/3642>
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mapAttrs' nameValuePair;
in
{
systemd.paths = mapAttrs' (
name: value:
nameValuePair "${name}-key" {
wantedBy = [ "paths.target" ];
pathConfig.PathExists = value.path;
}
) config.deployment.keys;
systemd.services = mapAttrs' (
name: value:
nameValuePair "${name}-key" {
enable = true;
serviceConfig = {
TimeoutStartSec = "infinity";
Restart = "always";
RestartSec = "100ms";
};
path = [ pkgs.inotify-tools ];
preStart = ''
(while read f; do if [ "$f" = "${value.name}" ]; then break; fi; done \
< <(inotifywait -qm --format '%f' -e create,move ${value.destDir}) ) &
if [[ -e "${value.path}" ]]; then
echo 'flapped down'
kill %1
exit 0
fi
wait %1
'';
script = ''
inotifywait -qq -e delete_self "${value.path}" &
if [[ ! -e "${value.path}" ]]; then
echo 'flapped up'
exit 0
fi
wait %1
'';
}
) config.deployment.keys;
}

View file

@ -1,396 +0,0 @@
with builtins;
rec {
keyType =
{
lib,
name,
config,
...
}:
let
inherit (lib) types;
in
{
options = {
name = lib.mkOption {
description = ''
File name of the key.
'';
default = name;
type = types.str;
};
text = lib.mkOption {
description = ''
Content of the key.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
type = types.nullOr types.str;
};
keyFile = lib.mkOption {
description = ''
Path of the local file to read the key from.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
apply = value: if value == null then null else toString value;
type = types.nullOr types.path;
};
keyCommand = lib.mkOption {
description = ''
Command to run to generate the key.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
type =
let
nonEmptyList = types.addCheck (types.listOf types.str) (l: length l > 0);
in
types.nullOr nonEmptyList;
};
destDir = lib.mkOption {
description = ''
Destination directory on the host.
'';
default = "/run/keys";
type = types.path;
};
path = lib.mkOption {
description = ''
Full path to the destination.
'';
default = "${config.destDir}/${config.name}";
type = types.path;
internal = true;
};
user = lib.mkOption {
description = ''
The group that will own the file.
'';
default = "root";
type = types.str;
};
group = lib.mkOption {
description = ''
The group that will own the file.
'';
default = "root";
type = types.str;
};
permissions = lib.mkOption {
description = ''
Permissions to set for the file.
'';
default = "0600";
type = types.str;
};
uploadAt = lib.mkOption {
description = ''
When to upload the keys.
- pre-activation (default): Upload the keys before activating the new system profile.
- post-activation: Upload the keys after successfully activating the new system profile.
For `colmena upload-keys`, all keys are uploaded at the same time regardless of the configuration here.
'';
default = "pre-activation";
type = types.enum [
"pre-activation"
"post-activation"
];
};
};
};
# Colmena-specific options
#
# Largely compatible with NixOps/Morph.
deploymentOptions =
{ name, lib, ... }:
let
inherit (lib) types;
in
{
options = {
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 {
description = ''
The target SSH node for deployment.
By default, the node's attribute name will be used.
If set to null, only local deployment will be supported.
'';
type = types.nullOr types.str;
default = name;
};
targetPort = lib.mkOption {
description = ''
The target SSH port for deployment.
By default, the port is the standard port (22) or taken
from your ssh_config.
'';
type = types.nullOr types.ints.unsigned;
default = null;
};
targetUser = lib.mkOption {
description = ''
The user to use to log into the remote node. If set to null, the
target user will not be specified in SSH invocations.
'';
type = types.nullOr types.str;
default = "root";
};
allowLocalDeployment = lib.mkOption {
description = ''
Allow the configuration to be applied locally on the host running
Colmena.
For local deployment to work, all of the following must be true:
- The node must be running NixOS.
- The node must have deployment.allowLocalDeployment set to true.
- The node's networking.hostName must match the hostname.
To apply the configurations locally, run `colmena apply-local`.
You can also set deployment.targetHost to null if the nost is not
accessible over SSH (only local deployment will be possible).
'';
type = types.bool;
default = false;
};
buildOnTarget = lib.mkOption {
description = ''
Whether to build the system profiles on the target node itself.
When enabled, Colmena will copy the derivation to the target
node and initiate the build there. This avoids copying back the
build results involved with the native distributed build
feature. Furthermore, the `build` goal will be equivalent to
the `push` goal. Since builds happen on the target node, the
results are automatically "pushed" and won't exist in the local
Nix store.
You can temporarily override per-node settings by passing
`--build-on-target` (enable for all nodes) or
`--no-build-on-target` (disable for all nodes) on the command
line.
'';
type = types.bool;
default = false;
};
tags = lib.mkOption {
description = ''
A list of tags for the node.
Can be used to select a group of nodes for deployment.
'';
type = types.listOf types.str;
default = [ ];
};
keys = lib.mkOption {
description = ''
A set of secrets to be deployed to the node.
Secrets are transferred to the node out-of-band and
never ends up in the Nix store.
'';
type = types.attrsOf (types.submodule keyType);
default = { };
};
replaceUnknownProfiles = lib.mkOption {
description = ''
Allow a configuration to be applied to a host running a profile we
have no knowledge of. By setting this option to false, you reduce
the likelyhood of rolling back changes made via another Colmena user.
Unknown profiles are usually the result of either:
- The node had a profile applied, locally or by another Colmena.
- The host running Colmena garbage-collecting the profile.
To force profile replacement on all targeted nodes during apply,
use the flag `--force-replace-unknown-profiles`.
'';
type = types.bool;
default = true;
};
privilegeEscalationCommand = lib.mkOption {
description = ''
Command to use to elevate privileges when activating the new profiles on SSH hosts.
This is used on SSH hosts when `deployment.targetUser` is not `root`.
The user must be allowed to use the command non-interactively.
'';
type = types.listOf types.str;
default = [
"sudo"
"-H"
"--"
];
};
sshOptions = lib.mkOption {
description = ''
Extra SSH options to pass to the SSH command.
'';
type = types.listOf types.str;
default = [ ];
};
};
};
};
# 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
metaOptions =
{ lib, ... }:
let
inherit (lib) types;
in
{
options = {
name = lib.mkOption {
description = ''
The name of the configuration.
'';
type = types.str;
default = "hive";
};
description = lib.mkOption {
description = ''
A short description for the configuration.
'';
type = types.str;
default = "A Colmena Hive";
};
nixpkgs = lib.mkOption {
description = ''
The pinned Nixpkgs package set. Accepts one of the following:
- A path to a Nixpkgs checkout
- The Nixpkgs lambda (e.g., import <nixpkgs>)
- An initialized Nixpkgs attribute set
This option must be specified when using Flakes.
'';
type = types.unspecified;
default = null;
};
nodeNixpkgs = lib.mkOption {
description = ''
Node-specific Nixpkgs pins.
'';
type = types.attrsOf types.unspecified;
default = { };
};
nodeSpecialArgs = lib.mkOption {
description = ''
Node-specific special args.
'';
type = types.attrsOf types.unspecified;
default = { };
};
machinesFile = lib.mkOption {
description = ''
Use the machines listed in this file when building this hive configuration.
If your Colmena host has nix configured to allow for remote builds
(for nix-daemon, your user being included in trusted-users)
you can set a machines file that will be passed to the underlying
nix-store command during derivation realization as a builders option.
For example, if you support multiple orginizations each with their own
build machine(s) you can ensure that builds only take place on your
local machine and/or the machines specified in this file.
See https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds
for the machine specification format.
This option is ignored when builds are initiated on the remote nodes
themselves via `deployment.buildOnTarget` or `--build-on-target`. To
still use the Nix distributed build functionality, configure the
builders on the target nodes with `nix.buildMachines`.
'';
default = null;
apply = value: if value == null then null else toString value;
type = types.nullOr types.path;
};
specialArgs = lib.mkOption {
description = ''
A set of special arguments to be passed to NixOS modules.
This will be merged into the `specialArgs` used to evaluate
the NixOS configurations.
'';
default = { };
type = types.attrsOf types.unspecified;
};
allowApplyAll = lib.mkOption {
description = ''
Whether to allow deployments without a node filter set.
If set to false, a node filter must be specified with `--on` when
deploying.
It helps prevent accidental deployments to the entire cluster
when tags are used (e.g., `@production` and `@staging`).
'';
default = true;
type = types.bool;
};
};
};
}

View file

@ -0,0 +1,151 @@
{ lib, name, ... }:
let
inherit (lib) mkOption;
inherit (lib.types)
attrsOf
bool
ints
listOf
nullOr
str
submodule
;
in
{
options = {
deployment = {
systemType = mkOption {
description = ''
System type used for this node, e.g. NixOS.
'';
default = "nixos";
# TODO: enum among all registered systems?
type = str;
};
targetHost = mkOption {
description = ''
The target SSH node for deployment.
By default, the node's attribute name will be used.
If set to null, only local deployment will be supported.
'';
type = nullOr str;
default = name;
};
targetPort = mkOption {
description = ''
The target SSH port for deployment.
By default, the port is the standard port (22) or taken
from your ssh_config.
'';
type = nullOr ints.unsigned;
default = null;
};
targetUser = mkOption {
description = ''
The user to use to log into the remote node. If set to null, the
target user will not be specified in SSH invocations.
'';
type = nullOr str;
default = "root";
};
allowLocalDeployment = mkOption {
description = ''
Allow the configuration to be applied locally on the host running
Colmena.
For local deployment to work, all of the following must be true:
- The node must be running NixOS.
- The node must have deployment.allowLocalDeployment set to true.
- The node's networking.hostName must match the hostname.
To apply the configurations locally, run `colmena apply-local`.
You can also set deployment.targetHost to null if the nost is not
accessible over SSH (only local deployment will be possible).
'';
type = bool;
default = false;
};
buildOnTarget = mkOption {
description = ''
Whether to build the system profiles on the target node itself.
When enabled, Colmena will copy the derivation to the target
node and initiate the build there. This avoids copying back the
build results involved with the native distributed build
feature. Furthermore, the `build` goal will be equivalent to
the `push` goal. Since builds happen on the target node, the
results are automatically "pushed" and won't exist in the local
Nix store.
You can temporarily override per-node settings by passing
`--build-on-target` (enable for all nodes) or
`--no-build-on-target` (disable for all nodes) on the command
line.
'';
type = bool;
default = false;
};
tags = mkOption {
description = ''
A list of tags for the node.
Can be used to select a group of nodes for deployment.
'';
type = listOf str;
default = [ ];
};
keys = mkOption {
description = ''
A set of secrets to be deployed to the node.
Secrets are transferred to the node out-of-band and
never ends up in the Nix store.
'';
type = attrsOf (submodule (import ./key.nix));
default = { };
};
replaceUnknownProfiles = mkOption {
description = ''
Allow a configuration to be applied to a host running a profile we
have no knowledge of. By setting this option to false, you reduce
the likelyhood of rolling back changes made via another Colmena user.
Unknown profiles are usually the result of either:
- The node had a profile applied, locally or by another Colmena.
- The host running Colmena garbage-collecting the profile.
To force profile replacement on all targeted nodes during apply,
use the flag `--force-replace-unknown-profiles`.
'';
type = bool;
default = true;
};
privilegeEscalationCommand = mkOption {
description = ''
Command to use to elevate privileges when activating the new profiles on SSH hosts.
This is used on SSH hosts when `deployment.targetUser` is not `root`.
The user must be allowed to use the command non-interactively.
'';
type = listOf str;
default = [
"sudo"
"-H"
"--"
];
};
sshOptions = mkOption {
description = ''
Extra SSH options to pass to the SSH command.
'';
type = listOf str;
default = [ ];
};
};
};
}

View file

@ -0,0 +1,120 @@
{
config,
lib,
name,
...
}:
let
inherit (lib) mkOption;
inherit (lib.types)
addCheck
enum
listOf
nullOr
path
str
;
in
{
options = {
name = mkOption {
description = ''
File name of the key.
'';
default = name;
type = str;
};
text = mkOption {
description = ''
Content of the key.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
type = nullOr str;
};
keyFile = mkOption {
description = ''
Path of the local file to read the key from.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
apply = value: if value == null then null else builtins.toString value;
type = nullOr path;
};
keyCommand = mkOption {
description = ''
Command to run to generate the key.
One of `text`, `keyCommand` and `keyFile` must be set.
'';
default = null;
type =
let
nonEmptyList = addCheck (listOf str) (l: builtins.length l > 0);
in
nullOr nonEmptyList;
};
destDir = mkOption {
description = ''
Destination directory on the host.
'';
default = "/run/keys";
type = path;
};
path = mkOption {
description = ''
Full path to the destination.
'';
default = "${config.destDir}/${config.name}";
type = path;
internal = true;
};
user = mkOption {
description = ''
The group that will own the file.
'';
default = "root";
type = str;
};
group = mkOption {
description = ''
The group that will own the file.
'';
default = "root";
type = str;
};
permissions = mkOption {
description = ''
Permissions to set for the file.
'';
default = "0600";
type = str;
};
uploadAt = mkOption {
description = ''
When to upload the keys.
- pre-activation (default): Upload the keys before activating the new system profile.
- post-activation: Upload the keys after successfully activating the new system profile.
For `colmena upload-keys`, all keys are uploaded at the same time regardless of the configuration here.
'';
default = "pre-activation";
type = enum [
"pre-activation"
"post-activation"
];
};
};
}

View file

@ -0,0 +1,114 @@
{ lib, ... }:
let
inherit (lib) mkOption;
inherit (lib.types)
attrsOf
bool
nullOr
path
str
unspecified
;
in
{
options = {
name = mkOption {
description = ''
The name of the configuration.
'';
type = str;
default = "hive";
};
description = mkOption {
description = ''
A short description for the configuration.
'';
type = str;
default = "A Colmena Hive";
};
nixpkgs = mkOption {
description = ''
The pinned Nixpkgs package set. Accepts one of the following:
- A path to a Nixpkgs checkout
- The Nixpkgs lambda (e.g., import <nixpkgs>)
- An initialized Nixpkgs attribute set
This option must be specified when using Flakes.
'';
type = unspecified;
default = null;
};
nodeNixpkgs = mkOption {
description = ''
Node-specific Nixpkgs pins.
'';
type = attrsOf unspecified;
default = { };
};
nodeSpecialArgs = mkOption {
description = ''
Node-specific special args.
'';
type = attrsOf unspecified;
default = { };
};
machinesFile = mkOption {
description = ''
Use the machines listed in this file when building this hive configuration.
If your Colmena host has nix configured to allow for remote builds
(for nix-daemon, your user being included in trusted-users)
you can set a machines file that will be passed to the underlying
nix-store command during derivation realization as a builders option.
For example, if you support multiple orginizations each with their own
build machine(s) you can ensure that builds only take place on your
local machine and/or the machines specified in this file.
See https://nixos.org/manual/nix/stable/advanced-topics/distributed-builds
for the machine specification format.
This option is ignored when builds are initiated on the remote nodes
themselves via `deployment.buildOnTarget` or `--build-on-target`. To
still use the Nix distributed build functionality, configure the
builders on the target nodes with `nix.buildMachines`.
'';
default = null;
apply = value: if value == null then null else builtins.toString value;
type = nullOr path;
};
specialArgs = mkOption {
description = ''
A set of special arguments to be passed to NixOS modules.
This will be merged into the `specialArgs` used to evaluate
the NixOS configurations.
'';
default = { };
type = attrsOf unspecified;
};
allowApplyAll = mkOption {
description = ''
Whether to allow deployments without a node filter set.
If set to false, a node filter must be specified with `--on` when
deploying.
It helps prevent accidental deployments to the entire cluster
when tags are used (e.g., `@production` and `@staging`).
'';
default = true;
type = bool;
};
};
}

View file

@ -0,0 +1,54 @@
{ lib, ... }:
let
inherit (lib) mkOption;
inherit (lib.types)
attrsOf
functionTo
submodule
unspecified
;
in
{
options.registry = mkOption {
description = ''
A registry of all system types.
'';
type = attrsOf (
submodule (
{ name, ... }:
{
options = {
evalConfig = mkOption {
description = ''
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 = functionTo unspecified;
};
supportsDeployment = mkOption {
description = ''
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 = mkOption {
description = ''
Default configuration for that system type.
'';
type = functionTo unspecified;
default = _: { };
};
};
}
)
);
};
}