M7.6: nisse-review's image
A `review` target in the Dockerfile: its own user (nisse-review, uid 10002), git for its clones, state and clones in /var/lib/nisse-review, configuration at /etc/nisse-review, and only its own agent installed. release.yml publishes gitea.dongar.net/dev/nisse-review beside nisse-dev, at the same version. review-bringup.sh builds and runs the image, as bringup.sh runs nisse-dev's. The test stage adds an agent package its base lacks, without its dependencies, since the suite's own code imports every agent's. Without that, the tests over nisse-dev's image no longer import since test_review_live arrived. tests/deploy/test_images.py checks the image and that the test stage leaves what it installed as it was. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
1 parent
5277d7827b
commit
2fb654f92b
6 files changed
+324
-35
No files matched your search
@@ -0,0 +1,96 @@
|
||||
"""Every agent is its own image (Development Plan §3, M7.6): nisse-review's
|
||||
runs as its own user with git and only its own agent, and the test stage
|
||||
over it keeps what it installed. Builds the images, so it needs Docker."""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).parent.parent.parent
|
||||
pytestmark = [
|
||||
pytest.mark.integration,
|
||||
pytest.mark.skipif(
|
||||
shutil.which("docker") is None
|
||||
or subprocess.run(
|
||||
["docker", "info"], capture_output=True, check=False
|
||||
).returncode,
|
||||
reason="needs Docker",
|
||||
),
|
||||
]
|
||||
|
||||
# The installed packages' files and their times, hashed: the same in an
|
||||
# image and the test stage over it means the tests ran against what was
|
||||
# built, not a reinstall (even one from the same source).
|
||||
HASH = (
|
||||
"import hashlib, pathlib, nisse_review, nisse_core; h = hashlib.sha256(); "
|
||||
"[h.update(p.read_bytes() + str(p.stat().st_mtime_ns).encode()) for m in (nisse_review, nisse_core) "
|
||||
"for p in sorted(pathlib.Path(m.__file__).parent.rglob('*.py'))]; "
|
||||
"print(h.hexdigest())"
|
||||
)
|
||||
|
||||
|
||||
def build(tag: str, *args: str) -> str:
|
||||
subprocess.run(
|
||||
["docker", "build", "--quiet", "-t", tag, *args, str(ROOT)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=1200,
|
||||
)
|
||||
return tag
|
||||
|
||||
|
||||
def sh(image: str, script: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["docker", "run", "--rm", "--entrypoint", "sh", image, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def review() -> str:
|
||||
return build("nisse-review:image-test", "--target", "review")
|
||||
|
||||
|
||||
def test_the_reviewer_runs_as_its_own_user_with_git(review: str) -> None:
|
||||
done = sh(review, "id -un; git --version; command -v nisse-review")
|
||||
user, git, entry = done.stdout.splitlines()
|
||||
assert user == "nisse-review" and git.startswith("git version")
|
||||
assert entry == "/opt/nisse/bin/nisse-review"
|
||||
inspected = subprocess.run(
|
||||
["docker", "inspect", "-f", "{{json .Config.Volumes}}", review],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout
|
||||
assert "/var/lib/nisse-review" in inspected
|
||||
|
||||
|
||||
def test_the_reviewer_image_holds_no_other_agent(review: str) -> None:
|
||||
assert sh(review, "python -c 'import nisse_review'").returncode == 0
|
||||
assert sh(review, "python -c 'import nisse_dev'").returncode != 0
|
||||
|
||||
|
||||
def test_its_state_is_writable_and_its_config_folder_is_not(review: str) -> None:
|
||||
assert sh(review, "touch /var/lib/nisse-review/x").returncode == 0
|
||||
assert sh(review, "touch /etc/nisse-review/x").returncode != 0
|
||||
|
||||
|
||||
def test_the_test_stage_over_it_keeps_what_it_installed(review: str) -> None:
|
||||
tests = build(
|
||||
"nisse-review:image-test-tests",
|
||||
"--target",
|
||||
"test",
|
||||
"--build-arg",
|
||||
f"TEST_BASE={review}",
|
||||
)
|
||||
both = sh(tests, "python -c 'import nisse_dev, nisse_review, pytest'")
|
||||
assert both.returncode == 0, both.stderr # the suite's imports resolve
|
||||
assert (
|
||||
sh(review, f'python -c "{HASH}"').stdout
|
||||
== sh(tests, f'python -c "{HASH}"').stdout
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""scripts/review-bringup.sh runs nisse-review from its image (M7.6): built
|
||||
from the code under test, with the reviewer's folder at its own path."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).parent.parent.parent / "scripts" / "review-bringup.sh"
|
||||
|
||||
|
||||
def _setup(tmp_path: Path, *, running: bool = False) -> dict[str, str]:
|
||||
"""A source repository with one commit, the reviewer's folder with its
|
||||
config, and stand-ins for docker, systemd-run and systemctl that log."""
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "Dockerfile").write_text("FROM scratch\n")
|
||||
for args in (
|
||||
["init", "-q"],
|
||||
["add", "."],
|
||||
["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "c"],
|
||||
):
|
||||
subprocess.run(["git", "-C", str(src), *args], check=True)
|
||||
review = tmp_path / "review"
|
||||
review.mkdir()
|
||||
(review / "nisse-review.yaml").write_text("state_db: x\n")
|
||||
(review / ".env").write_text("")
|
||||
bin_ = tmp_path / "bin"
|
||||
bin_.mkdir()
|
||||
log = tmp_path / "calls"
|
||||
stubs = {
|
||||
"docker": f'echo "docker $*" >> {log}',
|
||||
"systemd-run": f'echo "systemd-run $*" >> {log}',
|
||||
"systemctl": f'echo "systemctl $*" >> {log}; '
|
||||
f'[ "$1" = is-active ] && exit {0 if running else 3}; exit 0',
|
||||
}
|
||||
for name, body in stubs.items():
|
||||
path = bin_ / name
|
||||
path.write_text(f"#!/usr/bin/env bash\n{body}\n")
|
||||
path.chmod(0o755)
|
||||
return {
|
||||
"PATH": f"{bin_}:/usr/bin:/bin",
|
||||
"HOME": str(tmp_path),
|
||||
"NISSE_REVIEW": str(review),
|
||||
"NISSE_SRC": str(src),
|
||||
"LOG": str(log),
|
||||
}
|
||||
|
||||
|
||||
def _run(env: dict[str, str], *args: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT), *args],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def test_start_builds_the_review_image_and_runs_it(tmp_path: Path) -> None:
|
||||
env = _setup(tmp_path)
|
||||
review = env["NISSE_REVIEW"]
|
||||
done = _run(env, "start")
|
||||
assert done.returncode == 0, done.stderr
|
||||
calls = Path(env["LOG"]).read_text().splitlines()
|
||||
[build] = [c for c in calls if c.startswith("docker build")]
|
||||
assert "--target review" in build and build.endswith(f"{review}/code")
|
||||
assert (Path(review) / "code" / "Dockerfile").exists() # the code exported
|
||||
[run] = [c for c in calls if c.startswith("systemd-run")]
|
||||
assert "--unit=nisse-review-bringup" in run
|
||||
assert "--property=KillSignal=SIGTERM" in run
|
||||
assert "docker run --rm --name nisse-review-bringup" in run
|
||||
assert f"-v {review}:{review}" in run
|
||||
assert f"-e NISSE_CONFIG={review}/nisse-review.yaml" in run
|
||||
assert f"-e NISSE_ENV={review}/.env" in run
|
||||
assert run.endswith("nisse-review:bringup")
|
||||
sha = subprocess.run(
|
||||
["git", "-C", env["NISSE_SRC"], "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
assert (Path(review) / "run-info").read_text().strip() == sha
|
||||
|
||||
|
||||
def test_it_wont_start_a_second_reviewer(tmp_path: Path) -> None:
|
||||
env = _setup(tmp_path, running=True)
|
||||
done = _run(env, "start")
|
||||
assert done.returncode != 0 and "already running" in done.stderr
|
||||
assert "docker" not in Path(env["LOG"]).read_text()
|
||||
|
||||
|
||||
def test_plan_shows_what_start_would_run(tmp_path: Path) -> None:
|
||||
env = _setup(tmp_path)
|
||||
done = _run(env, "plan")
|
||||
assert done.returncode == 0, done.stderr
|
||||
assert "docker build --target review -t nisse-review:bringup" in done.stdout
|
||||
assert not Path(env["LOG"]).exists() # nothing run
|
||||
Reference in new issue
Block a user