feat(updater): remote self-update daemon with auto-rollback
Adds a separate `decnet updater` daemon on each worker that owns the agent's release directory and installs tarball pushes from the master over mTLS. A normal `/update` never touches the updater itself, so the updater is always a known-good rescuer if a bad agent push breaks /health — the rotation is reversed and the agent restarted against the previous release. `POST /update-self` handles updater upgrades explicitly (no auto-rollback). - decnet/updater/: executor, FastAPI app, uvicorn launcher - decnet/swarm/updater_client.py, tar_tree.py: master-side push - cli: `decnet updater`, `decnet swarm update [--host|--all] [--include-self] [--dry-run]`, `--updater` on `swarm enroll` - enrollment API issues a second cert (CN=updater@<host>) signed by the same CA; SwarmHost records updater_cert_fingerprint - tests: executor, app, CLI, tar tree, enroll-with-updater (37 new) - wiki: Remote-Updates page + sidebar + SWARM-Mode cross-link
This commit is contained in:
@@ -78,6 +78,36 @@ def test_enroll_creates_host_and_returns_bundle(client: TestClient) -> None:
|
||||
assert len(body["fingerprint"]) == 64 # sha256 hex
|
||||
|
||||
|
||||
def test_enroll_with_updater_issues_second_cert(client: TestClient, ca_dir) -> None:
|
||||
resp = client.post(
|
||||
"/swarm/enroll",
|
||||
json={"name": "worker-upd", "address": "10.0.0.99", "agent_port": 8765,
|
||||
"issue_updater_bundle": True},
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
body = resp.json()
|
||||
assert body["updater"] is not None
|
||||
assert body["updater"]["fingerprint"] != body["fingerprint"]
|
||||
assert "-----BEGIN CERTIFICATE-----" in body["updater"]["updater_cert_pem"]
|
||||
assert "-----BEGIN PRIVATE KEY-----" in body["updater"]["updater_key_pem"]
|
||||
# Cert bundle persisted on master.
|
||||
upd_bundle = ca_dir / "workers" / "worker-upd" / "updater"
|
||||
assert (upd_bundle / "updater.crt").is_file()
|
||||
assert (upd_bundle / "updater.key").is_file()
|
||||
# DB row carries the updater fingerprint.
|
||||
row = client.get(f"/swarm/hosts/{body['host_uuid']}").json()
|
||||
assert row.get("updater_cert_fingerprint") == body["updater"]["fingerprint"]
|
||||
|
||||
|
||||
def test_enroll_without_updater_omits_bundle(client: TestClient) -> None:
|
||||
resp = client.post(
|
||||
"/swarm/enroll",
|
||||
json={"name": "worker-no-upd", "address": "10.0.0.98", "agent_port": 8765},
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["updater"] is None
|
||||
|
||||
|
||||
def test_enroll_rejects_duplicate_name(client: TestClient) -> None:
|
||||
payload = {"name": "worker-dup", "address": "10.0.0.6", "agent_port": 8765}
|
||||
assert client.post("/swarm/enroll", json=payload).status_code == 201
|
||||
|
||||
Reference in New Issue
Block a user