Merge pull request 'vars: make all python tests work in nix sandbox' (#2502) from DavHau/clan-core:DavHau-dave into main

This commit is contained in:
clan-bot
2024-11-27 07:38:23 +00:00
10 changed files with 170 additions and 60 deletions

View File

@@ -1,4 +1,5 @@
import json
import os
from pathlib import Path
from clan_cli.clan_uri import FlakeId
@@ -16,6 +17,10 @@ def get_all_machines(flake: FlakeId, nix_options: list[str]) -> list[Machine]:
nix_build([f'{flake}#clanInternals.all-machines-json."{system}"'])
).stdout
tmp_store = os.environ.get("TMP_STORE", None)
if tmp_store:
json_path = f"{tmp_store}/{json_path}"
machines_json = json.loads(Path(json_path.rstrip()).read_text())
machines = []

View File

@@ -1,6 +1,7 @@
import importlib
import json
import logging
import os
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
@@ -329,6 +330,10 @@ class Machine:
return self._build_cache[attr]
output = self.nix("build", attr, extra_config, nix_options)
assert isinstance(output, Path), "Nix build did not result in a single path"
tmp_store = os.environ.get("TMP_STORE", None)
if tmp_store is not None:
output = Path(f"{tmp_store}/{output!s}")
if isinstance(output, Path):
self._build_cache[attr] = output
return output

View File

@@ -10,7 +10,11 @@ from clan_cli.errors import ClanError
def nix_command(flags: list[str]) -> list[str]:
return ["nix", "--extra-experimental-features", "nix-command flakes", *flags]
args = ["nix", "--extra-experimental-features", "nix-command flakes", *flags]
store = os.environ.get("TMP_STORE", None)
if store:
args += ["--store", store]
return args
def nix_flake_show(flake_url: str | Path) -> list[str]:

View File

@@ -150,14 +150,38 @@ python3.pkgs.buildPythonApplication {
'';
clan-pytest-with-core =
runCommand "clan-pytest-with-core"
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
{
nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies;
buildInputs = [
pkgs.bash
pkgs.coreutils
pkgs.nix
];
closureInfo = pkgs.closureInfo {
rootPaths = [
pkgs.bash
pkgs.coreutils
pkgs.jq.dev
pkgs.stdenv
pkgs.stdenvNoCC
];
};
}
''
cp -r ${source} ./src
chmod +w -R ./src
cd ./src
export CLAN_CORE=${clan-core-path}
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 PYTHONWARNINGS=error
export NIX_STATE_DIR=$TMPDIR/nix
export IN_NIX_SANDBOX=1
export PYTHONWARNINGS=error
export TMP_STORE=$TMPDIR/store
# required to prevent concurrent 'nix flake lock' operations
export LOCK_NIX=$TMPDIR/nix_lock
mkdir -p $TMP_STORE/nix/store
xargs cp --recursive --target "$TMP_STORE/nix/store" < "$closureInfo/store-paths"
nix-store --load-db --store $TMP_STORE < "$closureInfo/registration"
${pythonWithTestDeps}/bin/python -m pytest -m "not impure and with_core" ./tests
touch $out
'';

View File

@@ -14,30 +14,88 @@
let
flakeLock = lib.importJSON (self + /flake.lock);
flakeInputs = builtins.removeAttrs inputs [ "self" ];
flakeLockVendoredDeps = flakeLock // {
nodes =
flakeLock.nodes
// (lib.flip lib.mapAttrs flakeInputs (
name: _:
flakeLock.nodes.${name}
// {
locked = {
inherit (flakeLock.nodes.${name}.locked) narHash;
lastModified =
# lol, nixpkgs has a different timestamp on the fs???
if name == "nixpkgs" then 0 else 1;
path = "${inputs.${name}}";
type = "path";
};
}
));
flakeLockVendoredDeps =
flakeLock:
flakeLock
// {
nodes =
flakeLock.nodes
// (lib.flip lib.mapAttrs flakeInputs (
name: _:
# remove follows and let 'nix flake lock' re-compute it later
# (lib.removeAttrs flakeLock.nodes.${name} ["inputs"])
flakeLock.nodes.${name}
// {
locked = {
inherit (flakeLock.nodes.${name}.locked) narHash;
lastModified =
# lol, nixpkgs has a different timestamp on the fs???
if name == "nixpkgs" then 0 else 1;
path = "${inputs.${name}}";
type = "path";
};
}
));
};
clanCoreLock = flakeLockVendoredDeps flakeLock;
clanCoreLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON clanCoreLock);
clanCoreNode = {
inputs = lib.mapAttrs (name: _input: name) flakeInputs;
locked = {
lastModified = 1;
path = "${self}";
type = "path";
};
original = {
type = "tarball";
url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
};
};
flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps);
clanCoreWithVendoredDeps = pkgs.runCommand "clan-core-with-vendored-deps" { } ''
cp -r ${self} $out
chmod +w -R $out
cp ${flakeLockFile} $out/flake.lock
'';
# generate a lock file that nix will accept for our flake templates,
# in order to not require internet access during tests.
templateLock = clanCoreLock // {
nodes = clanCoreLock.nodes // {
clan-core = clanCoreNode;
nixpkgs-lib = clanCoreLock.nodes.nixpkgs; # required by flake-parts
root = clanCoreLock.nodes.root // {
inputs = clanCoreLock.nodes.root.inputs // {
clan-core = "clan-core";
nixpkgs = "nixpkgs";
nixpkgs-lib = "nixpkgs-lib";
};
};
};
};
templateLockFile = builtins.toFile "template-flake.lock" (builtins.toJSON templateLock);
clanCoreWithVendoredDeps =
pkgs.runCommand "clan-core-with-vendored-deps"
{
buildInputs = [
pkgs.findutils
pkgs.git
pkgs.jq
pkgs.nix
];
}
''
set -e
export HOME=$(realpath .)
export NIX_STATE_DIR=$HOME
cp -r ${self} $out
chmod +w -R $out
cp ${clanCoreLockFile} $out/flake.lock
nix flake lock $out --extra-experimental-features 'nix-command flakes'
clanCoreHash=$(nix hash path ${self} --extra-experimental-features 'nix-command')
for templateDir in $(find $out/templates -mindepth 1 -maxdepth 1 -type d); do
if ! [ -e "$templateDir/flake.nix" ]; then
continue
fi
cp ${templateLockFile} $templateDir/flake.lock
cat $templateDir/flake.lock | jq ".nodes.\"clan-core\".locked.narHash = \"$clanCoreHash\"" > $templateDir/flake.lock.final
mv $templateDir/flake.lock.final $templateDir/flake.lock
nix flake lock $templateDir --extra-experimental-features 'nix-command flakes'
done
'';
in
{
devShells.clan-cli = pkgs.callPackage ./shell.nix {

View File

@@ -11,11 +11,16 @@ from typing import Any, NamedTuple
import pytest
from clan_cli.dirs import nixpkgs_source
from clan_cli.locked_open import locked_open
from fixture_error import FixtureError
from root import CLAN_CORE
log = logging.getLogger(__name__)
lock_nix = os.environ.get("LOCK_NIX", "")
if not lock_nix:
lock_nix = tempfile.NamedTemporaryFile().name # NOQA: SIM115
# allows defining nested dictionary in a single line
def def_value() -> defaultdict:
@@ -151,13 +156,20 @@ class ClanFlake:
sp.run(["chmod", "+w", "-R", str(self.path)], check=True)
self.substitute()
if not (self.path / ".git").exists():
sp.run(
["nix", "flake", "lock"],
cwd=self.path,
check=True,
)
with pytest.MonkeyPatch.context() as mp:
init_git(mp, self.path)
with locked_open(Path(lock_nix), "w"):
sp.run(
[
"nix",
"flake",
"lock",
"--extra-experimental-features",
"flakes nix-command",
],
cwd=self.path,
check=True,
)
with pytest.MonkeyPatch.context() as mp:
init_git(mp, self.path)
def refresh(self) -> None:
if not self.path.exists():

View File

@@ -93,7 +93,7 @@ def test_required_generators() -> None:
]
@pytest.mark.impure
@pytest.mark.with_core
def test_generate_public_var(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -128,7 +128,7 @@ def test_generate_public_var(
assert json.loads(vars_eval) == "hello\n"
@pytest.mark.impure
@pytest.mark.with_core
def test_generate_secret_var_sops(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -166,7 +166,7 @@ def test_generate_secret_var_sops(
# TODO: it doesn't actually test if the group has access
@pytest.mark.impure
@pytest.mark.with_core
def test_generate_secret_var_sops_with_default_group(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -219,7 +219,7 @@ def test_generate_secret_var_sops_with_default_group(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_generated_shared_secret_sops(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -264,7 +264,7 @@ def test_generated_shared_secret_sops(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_generate_secret_var_password_store(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -316,7 +316,7 @@ def test_generate_secret_var_password_store(
assert "my_generator/my_secret" in vars_text
@pytest.mark.impure
@pytest.mark.with_core
def test_generate_secret_for_multiple_machines(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -378,7 +378,7 @@ def test_generate_secret_for_multiple_machines(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_dependant_generators(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -409,7 +409,7 @@ def test_dependant_generators(
)
@pytest.mark.impure
@pytest.mark.with_core
@pytest.mark.parametrize(
("prompt_type", "input_value"),
[
@@ -447,7 +447,7 @@ def test_prompt(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_share_flag(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -514,7 +514,7 @@ def test_share_flag(
assert json.loads(vars_eval) == "hello\n"
@pytest.mark.impure
@pytest.mark.with_core
def test_depending_on_shared_secret_succeeds(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -540,7 +540,7 @@ def test_depending_on_shared_secret_succeeds(
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
@pytest.mark.impure
@pytest.mark.with_core
def test_prompt_create_file(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -572,7 +572,7 @@ def test_prompt_create_file(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_api_get_prompts(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -595,7 +595,7 @@ def test_api_get_prompts(
assert api_prompts[0].prompts[0].previous_value == "input1"
@pytest.mark.impure
@pytest.mark.with_core
def test_api_set_prompts(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -634,7 +634,7 @@ def test_api_set_prompts(
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
@pytest.mark.impure
@pytest.mark.with_core
def test_commit_message(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -691,7 +691,7 @@ def test_commit_message(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_default_value(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -728,7 +728,7 @@ def test_default_value(
assert json.loads(value_eval) == "hello"
@pytest.mark.impure
@pytest.mark.with_core
def test_stdout_of_generate(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -809,7 +809,7 @@ def test_stdout_of_generate(
assert "hello" not in output.out
@pytest.mark.impure
@pytest.mark.with_core
def test_migration_skip(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -837,7 +837,7 @@ def test_migration_skip(
assert in_repo_store.get(Generator("my_generator"), "my_value").decode() == "world"
@pytest.mark.impure
@pytest.mark.with_core
def test_migration(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -873,7 +873,7 @@ def test_migration(
assert sops_store.get(Generator("my_generator"), "my_secret").decode() == "hello"
@pytest.mark.impure
@pytest.mark.with_core
def test_fails_when_files_are_left_from_other_backend(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -915,7 +915,7 @@ def test_fails_when_files_are_left_from_other_backend(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_keygen(
monkeypatch: pytest.MonkeyPatch,
temporary_home: Path,
@@ -935,7 +935,7 @@ def test_keygen(
assert (temporary_home / "sops" / "users" / "user").is_dir()
@pytest.mark.impure
@pytest.mark.with_core
def test_vars_get(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,
@@ -962,7 +962,7 @@ def test_vars_get(
)
@pytest.mark.impure
@pytest.mark.with_core
def test_invalidation(
monkeypatch: pytest.MonkeyPatch,
flake: ClanFlake,

View File

@@ -1,10 +1,10 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
clan.url = "git+https://git.clan.lol/clan/clan-core";
clan.inputs.nixpkgs.follows = "nixpkgs";
clan.inputs.flake-parts.follows = "flake-parts";
clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
clan-core.inputs.nixpkgs.follows = "nixpkgs";
clan-core.inputs.flake-parts.follows = "flake-parts";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";

View File

@@ -1,5 +1,6 @@
{
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core";
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, ... }:

View File

@@ -2,6 +2,7 @@
description = "<Put your description here>";
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, ... }: