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:
@@ -12,13 +12,14 @@ from __future__ import annotations
|
||||
|
||||
import uuid as _uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from decnet.swarm import pki
|
||||
from decnet.web.db.repository import BaseRepository
|
||||
from decnet.web.dependencies import get_repo
|
||||
from decnet.web.db.models import SwarmEnrolledBundle, SwarmEnrollRequest
|
||||
from decnet.web.db.models import SwarmEnrolledBundle, SwarmEnrollRequest, SwarmUpdaterBundle
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -46,6 +47,26 @@ async def api_enroll_host(
|
||||
bundle_dir = pki.DEFAULT_CA_DIR / "workers" / req.name
|
||||
pki.write_worker_bundle(issued, bundle_dir)
|
||||
|
||||
updater_view: Optional[SwarmUpdaterBundle] = None
|
||||
updater_fp: Optional[str] = None
|
||||
if req.issue_updater_bundle:
|
||||
updater_cn = f"updater@{req.name}"
|
||||
updater_sans = list({*sans, updater_cn, "127.0.0.1"})
|
||||
updater_issued = pki.issue_worker_cert(ca, updater_cn, updater_sans)
|
||||
# Persist alongside the worker bundle for replay.
|
||||
updater_dir = bundle_dir / "updater"
|
||||
updater_dir.mkdir(parents=True, exist_ok=True)
|
||||
(updater_dir / "updater.crt").write_bytes(updater_issued.cert_pem)
|
||||
(updater_dir / "updater.key").write_bytes(updater_issued.key_pem)
|
||||
import os as _os
|
||||
_os.chmod(updater_dir / "updater.key", 0o600)
|
||||
updater_fp = updater_issued.fingerprint_sha256
|
||||
updater_view = SwarmUpdaterBundle(
|
||||
fingerprint=updater_fp,
|
||||
updater_cert_pem=updater_issued.cert_pem.decode(),
|
||||
updater_key_pem=updater_issued.key_pem.decode(),
|
||||
)
|
||||
|
||||
host_uuid = str(_uuid.uuid4())
|
||||
await repo.add_swarm_host(
|
||||
{
|
||||
@@ -55,6 +76,7 @@ async def api_enroll_host(
|
||||
"agent_port": req.agent_port,
|
||||
"status": "enrolled",
|
||||
"client_cert_fingerprint": issued.fingerprint_sha256,
|
||||
"updater_cert_fingerprint": updater_fp,
|
||||
"cert_bundle_path": str(bundle_dir),
|
||||
"enrolled_at": datetime.now(timezone.utc),
|
||||
"notes": req.notes,
|
||||
@@ -69,4 +91,5 @@ async def api_enroll_host(
|
||||
ca_cert_pem=issued.ca_cert_pem.decode(),
|
||||
worker_cert_pem=issued.cert_pem.decode(),
|
||||
worker_key_pem=issued.key_pem.decode(),
|
||||
updater=updater_view,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user