imp: soc of eval, modules & options

- the reason for this change is to have more transparent separation
  of concern between effectuations of the module system and pre-module
  system effectuations

- with improved flakes support down the line, pre-module system
  effectuations will get more complex

- this also allows to patch the aspects of the evaluation individually
  while tracking other components from upstream. eg. path options & eval
  but not modules
This commit is contained in:
David Arnold 2022-04-03 19:07:14 -05:00 committed by Zhaofeng Li
parent ea4f2ba6dc
commit 9bd5e7bb25
5 changed files with 429 additions and 398 deletions

View file

@ -15,8 +15,11 @@
outputs = { self, nixpkgs, utils, ... }: let outputs = { self, nixpkgs, utils, ... }: let
supportedSystems = [ "x86_64-linux" "i686-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; supportedSystems = [ "x86_64-linux" "i686-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
colmenaOptions = import ./src/nix/hive/options.nix;
colmenaModules = import ./src/nix/hive/modules.nix;
evalNix = import ./src/nix/hive/eval.nix { evalNix = import ./src/nix/hive/eval.nix {
hermetic = true; hermetic = true;
inherit colmenaOptions colmenaModules;
}; };
in utils.lib.eachSystem supportedSystems (system: let in utils.lib.eachSystem supportedSystems (system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {

View file

@ -1,9 +1,12 @@
{ rawHive ? null # Colmena Hive attrset { rawHive ? null # Colmena Hive attrset
, flakeUri ? null # Nix Flake URI with `outputs.colmena` , flakeUri ? null # Nix Flake URI with `outputs.colmena`
, hermetic ? flakeUri != null # Whether we are allowed to use <nixpkgs> , hermetic ? flakeUri != null # Whether we are allowed to use <nixpkgs>
, colmenaOptions
, colmenaModules
}: }:
with builtins; with builtins;
let let
defaultHive = { defaultHive = {
# Will be set in defaultHiveMeta # Will be set in defaultHiveMeta
meta = {}; meta = {};
@ -14,292 +17,6 @@ let
defaults = {}; defaults = {};
}; };
# 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 = if !hermetic then <nixpkgs> else null;
};
nodeNixpkgs = lib.mkOption {
description = ''
Node-specific Nixpkgs pins.
'';
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/#chap-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;
};
};
};
# Colmena-specific options
#
# Largely compatible with NixOps/Morph.
deploymentOptions = { name, lib, ... }: let
inherit (lib) types;
in {
options = {
deployment = {
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 null, login as the
current user.
'';
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" "--" ];
};
};
};
};
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" ];
};
};
};
uncheckedHive = let uncheckedHive = let
flakeToHive = flakeUri: let flakeToHive = flakeUri: let
@ -326,12 +43,15 @@ let
# 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 = [ metaOptions uncheckedUserMeta ]; modules = [ colmenaOptions.metaOptions uncheckedUserMeta ];
}).config; }).config;
mergedHive = removeAttrs (defaultHive // uncheckedHive) [ "meta" "network" ]; mergedHive = removeAttrs (defaultHive // uncheckedHive) [ "meta" "network" ];
meta = { meta = {
meta = userMeta; meta =
if !hermetic && userMeta.nixpkgs == null
then userMeta // { nixpkgs = <nixpkgs>; }
else userMeta;
}; };
in mergedHive // meta; in mergedHive // meta;
@ -377,7 +97,7 @@ let
- A Nixpkgs attribute set - A Nixpkgs attribute set
''; '';
pkgs = let nixpkgs = let
# Can't rely on the module system yet # Can't rely on the module system yet
nixpkgsConf = nixpkgsConf =
if uncheckedUserMeta ? nixpkgs then uncheckedUserMeta.nixpkgs if uncheckedUserMeta ? nixpkgs then uncheckedUserMeta.nixpkgs
@ -385,25 +105,15 @@ let
else <nixpkgs>; else <nixpkgs>;
in mkNixpkgs "meta.nixpkgs" nixpkgsConf; in mkNixpkgs "meta.nixpkgs" nixpkgsConf;
lib = pkgs.lib; lib = nixpkgs.lib;
reservedNames = [ "defaults" "network" "meta" ]; reservedNames = [ "defaults" "network" "meta" ];
evalNode = name: configs: let evalNode = name: configs: let
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 pkgs; else nixpkgs;
evalConfig = import (npkgs.path + "/nixos/lib/eval-config.nix"); evalConfig = import (npkgs.path + "/nixos/lib/eval-config.nix");
assertionModule = { config, ... }: {
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;
};
# 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.
@ -422,101 +132,16 @@ let
lib.optional (length remainingKeys != 0) lib.optional (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}";
}; };
# 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 = {
bindsTo = [ "${name}-key.path" ];
serviceConfig = {
Restart = "on-failure";
};
path = [ pkgs.inotifyTools ];
script = ''
if [[ ! -e "${val.path}" ]]; then
>&2 echo "${val.path} does not exist"
exit 0
fi
inotifywait -qq -e delete_self "${val.path}"
>&2 echo "${val.path} disappeared"
'';
};
}) config.deployment.keys;
};
in evalConfig { in evalConfig {
inherit (npkgs) system; inherit (npkgs) system;
modules = [ modules = let
assertionModule in [
nixpkgsModule nixpkgsModule
keyChownModule colmenaModules.assertionModule
keyServiceModule colmenaModules.keyChownModule
deploymentOptions colmenaModules.keyServiceModule
colmenaOptions.deploymentOptions
hive.defaults hive.defaults
] ++ configs; ] ++ configs;
specialArgs = hive.meta.specialArgs // { specialArgs = hive.meta.specialArgs // {
@ -563,7 +188,7 @@ let
evalSelectedDrvPaths = names: lib.mapAttrs (k: v: v.drvPath) (evalSelected names); evalSelectedDrvPaths = names: lib.mapAttrs (k: v: v.drvPath) (evalSelected names);
introspect = function: function { introspect = function: function {
inherit pkgs lib; inherit nixpkgs lib;
nodes = uncheckedNodes; nodes = uncheckedNodes;
}; };
in { in {
@ -574,12 +199,12 @@ in {
meta = hive.meta; meta = hive.meta;
nixosModules = { inherit deploymentOptions; }; nixosModules = { inherit (colmenaOptions) deploymentOptions; };
docs = { docs = {
deploymentOptions = pkgs: let deploymentOptions = pkgs: let
eval = pkgs.lib.evalModules { eval = pkgs.lib.evalModules {
modules = [ deploymentOptions ]; modules = [ colmenaOptions.deploymentOptions ];
specialArgs = { specialArgs = {
name = "nixos"; name = "nixos";
nodes = {}; nodes = {};
@ -589,7 +214,7 @@ in {
metaOptions = pkgs: let metaOptions = pkgs: let
eval = pkgs.lib.evalModules { eval = pkgs.lib.evalModules {
modules = [ metaOptions ]; modules = [ colmenaOptions.metaOptions ];
}; };
in eval.options; in eval.options;
}; };

View file

@ -28,6 +28,8 @@ use crate::job::JobHandle;
mod tests; mod tests;
const HIVE_EVAL: &[u8] = include_bytes!("eval.nix"); const HIVE_EVAL: &[u8] = include_bytes!("eval.nix");
const HIVE_OPTIONS: &[u8] = include_bytes!("options.nix");
const HIVE_MODULES: &[u8] = include_bytes!("modules.nix");
#[derive(Debug)] #[derive(Debug)]
pub enum HivePath { pub enum HivePath {
@ -54,6 +56,12 @@ pub struct Hive {
/// Path to temporary file containing eval.nix. /// Path to temporary file containing eval.nix.
eval_nix: TempPath, eval_nix: TempPath,
/// Path to temporary file containing options.nix.
options_nix: TempPath,
/// Path to temporary file containing modules.nix.
modules_nix: TempPath,
/// Whether to pass --show-trace in Nix commands. /// Whether to pass --show-trace in Nix commands.
show_trace: bool, show_trace: bool,
@ -111,7 +119,11 @@ impl HivePath {
impl Hive { impl Hive {
pub fn new(path: HivePath) -> ColmenaResult<Self> { pub fn new(path: HivePath) -> ColmenaResult<Self> {
let mut eval_nix = NamedTempFile::new()?; let mut eval_nix = NamedTempFile::new()?;
let mut options_nix = NamedTempFile::new()?;
let mut modules_nix = NamedTempFile::new()?;
eval_nix.write_all(HIVE_EVAL).unwrap(); eval_nix.write_all(HIVE_EVAL).unwrap();
options_nix.write_all(HIVE_OPTIONS).unwrap();
modules_nix.write_all(HIVE_MODULES).unwrap();
let context_dir = path.context_dir(); let context_dir = path.context_dir();
@ -119,6 +131,8 @@ impl Hive {
path, path,
context_dir, context_dir,
eval_nix: eval_nix.into_temp_path(), eval_nix: eval_nix.into_temp_path(),
options_nix: options_nix.into_temp_path(),
modules_nix: modules_nix.into_temp_path(),
show_trace: false, show_trace: false,
machines_file: RwLock::new(None), machines_file: RwLock::new(None),
}) })
@ -342,16 +356,20 @@ impl Hive {
match self.path() { match self.path() {
HivePath::Legacy(path) => { HivePath::Legacy(path) => {
format!( format!(
"with builtins; let eval = import {}; hive = eval {{ rawHive = import {}; }}; in ", "with builtins; let eval = import {}; hive = eval {{ rawHive = import {}; colmenaOptions = import {}; colmenaModules = import {}; }}; in ",
self.eval_nix.to_str().unwrap(), self.eval_nix.to_str().unwrap(),
path.to_str().unwrap(), path.to_str().unwrap(),
self.options_nix.to_str().unwrap(),
self.modules_nix.to_str().unwrap(),
) )
} }
HivePath::Flake(flake) => { HivePath::Flake(flake) => {
format!( format!(
"with builtins; let eval = import {}; hive = eval {{ flakeUri = \"{}\"; }}; in ", "with builtins; let eval = import {}; hive = eval {{ flakeUri = \"{}\"; colmenaOptions = import {}; colmenaModules = import {}; }}; in ",
self.eval_nix.to_str().unwrap(), self.eval_nix.to_str().unwrap(),
flake.uri(), flake.uri(),
self.options_nix.to_str().unwrap(),
self.modules_nix.to_str().unwrap(),
) )
} }
} }

98
src/nix/hive/modules.nix Normal file
View file

@ -0,0 +1,98 @@
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 = {
bindsTo = [ "${name}-key.path" ];
serviceConfig = {
Restart = "on-failure";
};
path = [ pkgs.inotifyTools ];
script = ''
if [[ ! -e "${val.path}" ]]; then
>&2 echo "${val.path} does not exist"
exit 0
fi
inotifywait -qq -e delete_self "${val.path}"
>&2 echo "${val.path} disappeared"
'';
};
}) config.deployment.keys;
};
}

287
src/nix/hive/options.nix Normal file
View file

@ -0,0 +1,287 @@
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 = {
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 null, login as the
current user.
'';
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" "--" ];
};
};
};
};
# 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 = {};
};
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/#chap-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;
};
};
};
}