forked from DGNum/colmena
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:
parent
ea4f2ba6dc
commit
9bd5e7bb25
5 changed files with 429 additions and 398 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
98
src/nix/hive/modules.nix
Normal 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
287
src/nix/hive/options.nix
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue