Files
MoShiro/scripts/test-container-updater.py
T
justme 83e5120377 refactor!: rename the project from Monitor to MoShiro
Namespaces, assemblies, projects and directories, the solution, binaries,
images, systemd units, config paths, environment variables, protocol method
names, the media type and the raw chunk magic all move to MoShiro/moshiro.
Existing data and deployments are not compatible and must be recreated.
Also fix the architecture tests after the solution layout change and make
the update tests independent of their order.
2026-10-07 01:49:04 +08:00

123 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Signed updater regression checks using fake engine/service CLIs and real signatures."""
import base64
import json
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
SCRIPT = Path(__file__).resolve().parents[1] / "packaging/update/moshiro-update.sh"
with tempfile.TemporaryDirectory(prefix="moshiro-updater-test-") as directory:
root = Path(directory)
(root / "bin").mkdir()
(root / "data/update").mkdir(parents=True)
(root / "conf").mkdir()
key = root / "key.pem"
pub = root / "key.pub"
subprocess.run(["openssl", "genpkey", "-algorithm", "EC", "-pkeyopt", "ec_paramgen_curve:P-256", "-out", str(key)], check=True, capture_output=True)
subprocess.run(["openssl", "pkey", "-in", str(key), "-pubout", "-out", str(pub)], check=True, capture_output=True)
log = root / "calls"
mode = root / "mode"
# Only external effects are replaced; verification and state handling run unchanged.
for name in ("podman", "docker", "systemctl", "sleep"):
tool = root / "bin" / name
tool.write_text("""#!/bin/sh
printf '%s %s\n' "${0##*/}" "$*" >> "$TEST_LOG"
case "${0##*/}" in
sleep) exit 0 ;;
podman|docker)
[ "$(cat "$TEST_MODE")" != pull-fail ] || exit 1
if [ -n "${DOCKER_CONFIG:-}" ]; then test -r "$DOCKER_CONFIG/config.json" || exit 1; fi ;;
systemctl)
[ "${1:-}" != --user ] || shift
case "$1" in
show) echo 0 ;;
is-active) [ "$(cat "$TEST_MODE")" != unhealthy ] ;;
esac ;;
esac
""")
tool.chmod(0o755)
env = dict(os.environ, PATH=str(root / "bin") + os.pathsep + os.environ["PATH"], MOSHIRO_UPDATE_CONF_DIR=str(root / "conf"), TEST_LOG=str(log), TEST_MODE=str(mode))
repo = "registry.example/moshiro/moshiro-agent"
digest = "sha256:" + "a" * 64
image_file = root / "agent.image"
state = root / "state"
auth = root / "auth file.json"
auth.write_text('{"auths":{}}')
def configure(engine="podman", scope="system", policy="manual", explicit=True):
values = dict(DATA_DIR=str(root / "data"), IMAGE_REPO=repo, UNIT="moshiro-agent.service", PUBKEY=str(pub), ENV_FILE=str(image_file), STATE_DIR=str(state), POLICY=policy, AUTHFILE=str(auth))
if explicit:
values.update(ENGINE=engine, SERVICE_SCOPE=scope)
(root / "conf/agent.conf").write_text("".join(k + "=" + shlex.quote(v) + "\n" for k, v in values.items()))
image_file.write_text("IMAGE=old-image\n")
(state / "agent.installed").unlink(missing_ok=True)
log.write_text("")
mode.write_text("healthy")
def offer(seq=1, tamper=False):
manifest = root / "data/update/offer.manifest"
manifest.write_text(json.dumps(dict(component="agent", image=repo, digest=digest, tag="v1.0.0", version="1.0.0", seq=seq), separators=(",", ":")))
sig = subprocess.check_output(["openssl", "dgst", "-sha256", "-sign", str(key), str(manifest)])
(root / "data/update/offer.sig").write_bytes(base64.b64encode(sig))
if tamper:
manifest.write_text(manifest.read_text().replace("1.0.0", "9.0.0"))
def invoke(command):
result = subprocess.run(["sh", str(SCRIPT), command, "agent"], env=env, capture_output=True, text=True)
status = json.loads((root / "data/update-status/status.json").read_text())
return result, status
configure(explicit=False)
offer()
result, status = invoke("apply")
assert result.returncode == 0 and status["state"] == "up_to_date", result.stderr
assert "systemctl restart" in log.read_text() and "podman pull --quiet --authfile" in log.read_text()
print("PASS legacy rootful Podman config")
configure(scope="user", policy="auto")
offer()
result, status = invoke("check")
assert result.returncode == 0 and status["state"] == "up_to_date", result.stderr
assert "systemctl --user restart" in log.read_text()
assert image_file.read_text() == "IMAGE=" + repo + "@" + digest + "\n"
print("PASS rootless Podman auto policy and user service restart")
log.write_text("")
_, status = invoke("check")
assert status["state"] == "up_to_date" and not log.read_text()
print("PASS replayed manifest does not pull or restart")
configure(engine="docker")
offer()
result, status = invoke("apply")
assert result.returncode == 0 and status["state"] == "up_to_date", result.stderr
assert "docker pull --quiet" in log.read_text() and "--authfile" not in log.read_text()
assert "systemctl --user" not in log.read_text()
print("PASS Docker auth config and system service restart")
configure(scope="user")
offer(tamper=True)
result, status = invoke("apply")
assert result.returncode != 0 and status["state"] == "invalid" and not log.read_text()
print("PASS invalid signature never invokes engine/service")
configure(scope="user")
offer()
mode.write_text("unhealthy")
result, status = invoke("apply")
assert result.returncode != 0 and status["state"] == "rolled_back"
assert image_file.read_text() == "IMAGE=old-image\n"
assert log.read_text().count("systemctl --user restart") == 2
assert not (state / "agent.installed").exists()
print("PASS unhealthy image rolls back without advancing sequence")
configure(engine="docker")
offer()
mode.write_text("pull-fail")
result, status = invoke("apply")
assert result.returncode != 0 and status["state"] == "failed"
assert image_file.read_text() == "IMAGE=old-image\n" and "systemctl" not in log.read_text()
print("PASS failed pull leaves active image unchanged")