clan tests: allow testing CLI interactions
This is an improvement of the clanTest nixos vm test module. The module now has a new option clan.test.fromFlake that allows to specify a flake.nix as the source for the test clan instead of specifying clan.XXX options. This in turn allows accessing the `flake.nix` inside the test driver allowing to use the clan cli on it
This commit is contained in:
61
checks/dummy-inventory-test-from-flake/default.nix
Normal file
61
checks/dummy-inventory-test-from-flake/default.nix
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
pkgs,
|
||||
nixosLib,
|
||||
clan-core,
|
||||
...
|
||||
}:
|
||||
|
||||
nixosLib.runTest (
|
||||
{ hostPkgs, config, ... }:
|
||||
{
|
||||
imports = [
|
||||
clan-core.modules.nixosVmTest.clanTest
|
||||
];
|
||||
|
||||
hostPkgs = pkgs;
|
||||
|
||||
# This tests the compatibility of the inventory
|
||||
# With the test framework
|
||||
# - legacy-modules
|
||||
# - clan.service modules
|
||||
name = "dummy-inventory-test-from-flake";
|
||||
|
||||
clan.test.fromFlake = ./.;
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
''
|
||||
${clan-core.legacyPackages.${hostPkgs.system}.setupNixInNixPython}
|
||||
|
||||
def run_clan(cmd: list[str], **kwargs) -> str:
|
||||
import subprocess
|
||||
clan = "${clan-core.packages.${hostPkgs.system}.clan-cli}/bin/clan"
|
||||
clan_args = ["--flake", "${config.clan.test.flakeForSandbox}"]
|
||||
return subprocess.run(
|
||||
["${hostPkgs.util-linux}/bin/unshare", "--user", "--map-user", "1000", "--map-group", "1000", clan, *cmd, *clan_args],
|
||||
**kwargs,
|
||||
check=True,
|
||||
).stdout
|
||||
|
||||
start_all()
|
||||
admin1.wait_for_unit("multi-user.target")
|
||||
peer1.wait_for_unit("multi-user.target")
|
||||
# Provided by the legacy module
|
||||
print(admin1.succeed("systemctl status dummy-service"))
|
||||
print(peer1.succeed("systemctl status dummy-service"))
|
||||
|
||||
# peer1 should have the 'hello' file
|
||||
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}")
|
||||
|
||||
ls_out = peer1.succeed("ls -la ${nodes.peer1.clan.core.vars.generators.new-service.files.a-secret.path}")
|
||||
# Check that the file is owned by 'nobody'
|
||||
assert "nobody" in ls_out, f"File is not owned by 'nobody': {ls_out}"
|
||||
# Check that the file is in the 'users' group
|
||||
assert "users" in ls_out, f"File is not in the 'users' group: {ls_out}"
|
||||
# Check that the file is in the '0644' mode
|
||||
assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}"
|
||||
|
||||
run_clan(["machines", "list"])
|
||||
'';
|
||||
}
|
||||
)
|
||||
70
checks/dummy-inventory-test-from-flake/flake.nix
Normal file
70
checks/dummy-inventory-test-from-flake/flake.nix
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
# Usage see: https://docs.clan.lol
|
||||
clan = clan-core.clanLib.buildClan {
|
||||
inherit self;
|
||||
|
||||
inventory =
|
||||
{ ... }:
|
||||
{
|
||||
meta.name = "foo";
|
||||
machines.peer1 = { };
|
||||
machines.admin1 = { };
|
||||
services = {
|
||||
legacy-module.default = {
|
||||
roles.peer.machines = [ "peer1" ];
|
||||
roles.admin.machines = [ "admin1" ];
|
||||
};
|
||||
};
|
||||
|
||||
instances."test" = {
|
||||
module.name = "new-service";
|
||||
roles.peer.machines.peer1 = { };
|
||||
};
|
||||
|
||||
modules = {
|
||||
legacy-module = ./legacy-module;
|
||||
};
|
||||
};
|
||||
|
||||
modules.new-service = {
|
||||
_class = "clan.service";
|
||||
manifest.name = "new-service";
|
||||
roles.peer = { };
|
||||
perMachine = {
|
||||
nixosModule = {
|
||||
# This should be generated by:
|
||||
# nix run .#generate-test-vars -- checks/dummy-inventory-test dummy-inventory-test
|
||||
clan.core.vars.generators.new-service = {
|
||||
files.not-a-secret = {
|
||||
secret = false;
|
||||
deploy = true;
|
||||
};
|
||||
files.a-secret = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
owner = "nobody";
|
||||
group = "users";
|
||||
mode = "0644";
|
||||
};
|
||||
script = ''
|
||||
# This is a dummy script that does nothing
|
||||
echo -n "not-a-secret" > $out/not-a-secret
|
||||
echo -n "a-secret" > $out/a-secret
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
# all machines managed by Clan
|
||||
inherit (clan) nixosConfigurations nixosModules clanInternals;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description = "Set up dummy-module"
|
||||
categories = ["System"]
|
||||
features = [ "inventory" ]
|
||||
|
||||
[constraints]
|
||||
roles.admin.min = 1
|
||||
roles.admin.max = 1
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
imports = [
|
||||
../shared.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
imports = [
|
||||
../shared.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
systemd.services.dummy-service = {
|
||||
enable = true;
|
||||
description = "Dummy service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
generated_password_path="${config.clan.core.vars.generators.dummy-generator.files.generated-password.path}"
|
||||
if [ ! -f "$generated_password_path" ]; then
|
||||
echo "Generated password file not found: $generated_password_path"
|
||||
exit 1
|
||||
fi
|
||||
host_id_path="${config.clan.core.vars.generators.dummy-generator.files.host-id.path}"
|
||||
if [ ! -e "$host_id_path" ]; then
|
||||
echo "Host ID file not found: $host_id_path"
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# TODO: add and prompt and make it work in the test framework
|
||||
clan.core.vars.generators.dummy-generator = {
|
||||
files.host-id.secret = false;
|
||||
files.generated-password.secret = true;
|
||||
script = ''
|
||||
echo $RANDOM > "$out"/host-id
|
||||
echo $RANDOM > "$out"/generated-password
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -51,6 +51,7 @@ in
|
||||
postgresql = self.clanLib.test.containerTest ./postgresql nixosTestArgs;
|
||||
|
||||
dummy-inventory-test = import ./dummy-inventory-test nixosTestArgs;
|
||||
dummy-inventory-test-from-flake = import ./dummy-inventory-test-from-flake nixosTestArgs;
|
||||
data-mesher = import ./data-mesher nixosTestArgs;
|
||||
}
|
||||
// lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
let
|
||||
inherit (lib)
|
||||
flatten
|
||||
flip
|
||||
mapAttrs'
|
||||
mapAttrsToList
|
||||
mkForce
|
||||
mkIf
|
||||
mkOption
|
||||
removePrefix
|
||||
types
|
||||
unique
|
||||
;
|
||||
@@ -24,9 +23,48 @@ in
|
||||
flake.modules.nixosVmTest.clanTest =
|
||||
{ config, hostPkgs, ... }:
|
||||
let
|
||||
clanFlakeResult = config.clan;
|
||||
testName = config.name;
|
||||
|
||||
clan-core = self;
|
||||
|
||||
inherit (lib)
|
||||
filterAttrs
|
||||
flip
|
||||
hasPrefix
|
||||
intersectAttrs
|
||||
mapAttrs'
|
||||
pathExists
|
||||
removePrefix
|
||||
throwIf
|
||||
;
|
||||
|
||||
# only relevant if config.clan.test.fromFlake is used
|
||||
importFlake =
|
||||
flakeDir:
|
||||
let
|
||||
flakeExpr = import (flakeDir + "/flake.nix");
|
||||
inputs = intersectAttrs flakeExpr.inputs clan-core.inputs;
|
||||
flake = flakeExpr.outputs (
|
||||
inputs
|
||||
// {
|
||||
self = flake // {
|
||||
outPath = flakeDir;
|
||||
};
|
||||
clan-core = clan-core;
|
||||
}
|
||||
);
|
||||
in
|
||||
throwIf (pathExists (
|
||||
flakeDir + "/flake.lock"
|
||||
)) "Test ${testName} should not have a flake.lock file" flake;
|
||||
|
||||
clanFlakeResult =
|
||||
if config.clan.test.fromFlake != null then importFlake config.clan.test.fromFlake else config.clan;
|
||||
|
||||
machineModules = flip filterAttrs clanFlakeResult.nixosModules (
|
||||
name: _module: hasPrefix "clan-machine-" name
|
||||
);
|
||||
|
||||
update-vars-script = "${
|
||||
self.packages.${hostPkgs.system}.generate-test-vars
|
||||
}/bin/generate-test-vars";
|
||||
@@ -48,7 +86,7 @@ in
|
||||
);
|
||||
|
||||
vars-check =
|
||||
hostPkgs.runCommand "update-vars-check"
|
||||
hostPkgs.runCommand "update-vars-check-${testName}"
|
||||
{
|
||||
nativeBuildInputs = generatorRuntimeInputs ++ [
|
||||
hostPkgs.nix
|
||||
@@ -89,6 +127,16 @@ in
|
||||
fi
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# the test's flake.nix with locked clan-core input
|
||||
flakeForSandbox = hostPkgs.runCommand "offline-flake-for-test-${config.name}" { } ''
|
||||
cp -r ${config.clan.directory} $out
|
||||
chmod +w -R $out
|
||||
substituteInPlace $out/flake.nix \
|
||||
--replace-fail \
|
||||
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz" \
|
||||
"${clan-core.packages.${hostPkgs.system}.clan-core-flake}"
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -115,6 +163,9 @@ in
|
||||
nixpkgs
|
||||
nix-darwin
|
||||
;
|
||||
# By default clan.directory defaults to self, but we don't
|
||||
# have a sensible default for self here
|
||||
self = throw "set clan.directory in the test";
|
||||
};
|
||||
modules = [
|
||||
clanLib.buildClanModule.flakePartsModule
|
||||
@@ -134,6 +185,28 @@ in
|
||||
type = types.bool;
|
||||
description = "Whether to use containers for the test.";
|
||||
};
|
||||
test.fromFlake = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.path;
|
||||
description = ''
|
||||
path to a directory containing a `flake.nix` defining the clan
|
||||
|
||||
Only use this if the clan CLI needs to be used inside the test.
|
||||
Otherwise, use the other clan.XXX options instead to specify the clan.
|
||||
|
||||
Loads the clan from a flake instead of using clan.XXX options.
|
||||
This has the benefit that a real flake.nix will be available in the test.
|
||||
This is useful to test CLI interactions which require a flake.nix.
|
||||
|
||||
Using this introduces dependencies that should otherwise be avoided.
|
||||
'';
|
||||
};
|
||||
test.flakeForSandbox = mkOption {
|
||||
default = flakeForSandbox;
|
||||
type = types.path;
|
||||
description = "The flake.nix to use for the test.";
|
||||
readOnly = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
@@ -141,10 +214,12 @@ in
|
||||
};
|
||||
};
|
||||
config = {
|
||||
clan.directory = mkIf (config.clan.test.fromFlake != null) (mkForce config.clan.test.fromFlake);
|
||||
|
||||
# Inherit all nodes from the clan
|
||||
# i.e. nodes.jon <- clan.machines.jon
|
||||
# clanInternals.nixosModules contains nixosModules per node
|
||||
nodes = flip mapAttrs' clanFlakeResult.nixosModules (
|
||||
nodes = flip mapAttrs' machineModules (
|
||||
name: machineModule: {
|
||||
name = removePrefix "clan-machine-" name;
|
||||
value = machineModule;
|
||||
@@ -169,11 +244,6 @@ in
|
||||
clanLib.test.sopsModule
|
||||
];
|
||||
|
||||
# Disable documentation
|
||||
# This is nice to speed up the evaluation
|
||||
# And also suppresses any warnings or errors about the documentation
|
||||
documentation.enable = lib.mkDefault false;
|
||||
|
||||
# Disable garbage collection during the test
|
||||
# https://nix.dev/manual/nix/2.28/command-ref/conf-file.html?highlight=min-free#available-settings
|
||||
nix.settings.min-free = 0;
|
||||
|
||||
@@ -8,4 +8,5 @@
|
||||
nix.registry = lib.mkForce { };
|
||||
documentation.doc.enable = false;
|
||||
documentation.man.enable = false;
|
||||
documentation.enable = lib.mkDefault false;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
{
|
||||
perSystem = {
|
||||
legacyPackages.setupNixInNix = ''
|
||||
export HOME=$TMPDIR
|
||||
export NIX_STATE_DIR=$TMPDIR/nix
|
||||
export NIX_CONF_DIR=$TMPDIR/etc
|
||||
export IN_NIX_SANDBOX=1
|
||||
export CLAN_TEST_STORE=$TMPDIR/store
|
||||
# required to prevent concurrent 'nix flake lock' operations
|
||||
export LOCK_NIX=$TMPDIR/nix_lock
|
||||
mkdir -p "$CLAN_TEST_STORE/nix/store"
|
||||
mkdir -p "$CLAN_TEST_STORE/nix/var/nix/gcroots"
|
||||
if [[ -n "''${closureInfo-}" ]]; then
|
||||
xargs cp --recursive --target "$CLAN_TEST_STORE/nix/store" < "$closureInfo/store-paths"
|
||||
nix-store --load-db --store "$CLAN_TEST_STORE" < "$closureInfo/registration"
|
||||
fi
|
||||
'';
|
||||
legacyPackages = {
|
||||
setupNixInNix = ''
|
||||
export HOME=$TMPDIR
|
||||
export NIX_STATE_DIR=$TMPDIR/nix
|
||||
export NIX_CONF_DIR=$TMPDIR/etc
|
||||
export IN_NIX_SANDBOX=1
|
||||
export CLAN_TEST_STORE=$TMPDIR/store
|
||||
# required to prevent concurrent 'nix flake lock' operations
|
||||
export LOCK_NIX=$TMPDIR/nix_lock
|
||||
mkdir -p "$CLAN_TEST_STORE/nix/store"
|
||||
mkdir -p "$CLAN_TEST_STORE/nix/var/nix/gcroots"
|
||||
if [[ -n "''${closureInfo-}" ]]; then
|
||||
xargs cp --recursive --target "$CLAN_TEST_STORE/nix/store" < "$closureInfo/store-paths"
|
||||
nix-store --load-db --store "$CLAN_TEST_STORE" < "$closureInfo/registration"
|
||||
fi
|
||||
'';
|
||||
setupNixInNixPython = ''
|
||||
from os import environ
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
environ['HOME'] = environ['TMPDIR']
|
||||
environ['NIX_STATE_DIR'] = environ['TMPDIR'] + '/nix'
|
||||
environ['NIX_CONF_DIR'] = environ['TMPDIR'] + '/etc'
|
||||
environ['IN_NIX_SANDBOX'] = '1'
|
||||
environ['CLAN_TEST_STORE'] = environ['TMPDIR'] + '/store'
|
||||
environ['LOCK_NIX'] = environ['TMPDIR'] + '/nix_lock'
|
||||
Path(environ['CLAN_TEST_STORE'] + '/nix/store').mkdir(parents=True, exist_ok=True)
|
||||
Path(environ['CLAN_TEST_STORE'] + '/nix/var/nix/gcroots').mkdir(parents=True, exist_ok=True)
|
||||
if 'closureInfo' in environ:
|
||||
subprocess.run(['cp', '--recursive', '--target', environ['CLAN_TEST_STORE'] + '/nix/store'] + environ['closureInfo'].split(), check=True)
|
||||
subprocess.run(['nix-store', '--load-db', '--store', environ['CLAN_TEST_STORE']] + ['<', environ['closureInfo'] + '/registration'], shell=True, check=True)
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user