mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 05:36:15 +00:00
feat: monky-deployd v0.1.0 — pull agent over the mesh (ADR-0028)
Stdlib-only Python 3.12 agent for docker VMs and laptops: flock → checkin
(bearer = the agent's OpenBao token, bootstrapped from the install kit's
jwt-tenancy deploy grant) → action apply|none|down → bundle (sha256
verified) → refusal checks (unresolved ${VAR} names only, manifest paths
pinned to monky/data/<env>/see/, privileged/host-network, rollback, disk
need×1.5+headroom) → lease → POST /v1/auth/jwt-tenancy/login → KV reads →
.env 0600 → promote → compose pull/up → wait healthy → report; finally
renew-self / re-lease before max TTL, scrub. Exit 0/75/78/1. Redactor log
filter. Transports sdk (openziti) / proxy (ziti tunnel proxy 18443/18200) /
system. Laptop mode.
Packaging: hardened oneshot + 60 s timer + proxy unit, nfpm .deb with
/opt/monky-deployd/venv, install.sh for Ubuntu 26.04 (Gitea release
download, enrol, ACLs, bootstrap from stdin), ansible role skeleton for
osg1-07. CI: lint/test on every change; wheel (openziti on ubuntu:26.04) and
package (nfpm) allow_failure until runner egress is proven; GitLab release +
release:gitea on v* tags. Docs: README, PROTOCOL, OPERATIONS, CHANGELOG,
CLAUDE/AGENTS.
Divergence noted: monky-tenancy main (MR !15) still ships the AppRole lease
and kit; this agent implements the plan's Gate 1 RESULT (login_jwt, no
unwrap) and refuses an AppRole lease loudly (LEASE_SHAPE).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from monky_deployd import cli
|
||||
from monky_deployd.agent import EX_OK
|
||||
|
||||
|
||||
def write_cfg(cfg):
|
||||
lines = [
|
||||
f"env_id: {cfg.env_id}",
|
||||
f"site: {cfg.site}",
|
||||
"transport: system",
|
||||
"tenancy:",
|
||||
f" host: {cfg.tenancy.host}",
|
||||
f" port: {cfg.tenancy.port}",
|
||||
" scheme: http",
|
||||
"bao:",
|
||||
f" addr: {cfg.bao.addr}",
|
||||
" ca_bundle: none",
|
||||
f"state_dir: {cfg.state_dir}",
|
||||
f"bootstrap_path: {cfg.bootstrap_path}",
|
||||
f"deploy_dir: {cfg.deploy_dir}",
|
||||
"healthy_timeout_s: 3",
|
||||
]
|
||||
Path(cfg.path).write_text("\n".join(lines) + "\n")
|
||||
return cfg.path
|
||||
|
||||
|
||||
def test_run_once_then_status(bootstrapped, tenancy, fake_docker, capsys):
|
||||
path = write_cfg(bootstrapped)
|
||||
assert cli.main(["-c", path, "run", "--once"]) == EX_OK
|
||||
assert cli.main(["-c", path, "status", "--json"]) == EX_OK
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert out["in_sync"] is True and out["applied_sha"] == tenancy.desired_sha and out["token_present"] is True
|
||||
assert out["healthy"] is True and out["containers"][0]["name"] == "see"
|
||||
assert cli.main(["-c", path, "status"]) == EX_OK
|
||||
text = capsys.readouterr().out
|
||||
assert "in sync" in text and "healthy" in text and "hvs." not in text
|
||||
|
||||
|
||||
def test_bootstrap_command(bootstrapped, bao, capsys):
|
||||
cfg = bootstrapped
|
||||
path = write_cfg(cfg)
|
||||
assert cli.main(["-c", path, "bootstrap"]) == EX_OK
|
||||
assert cfg.token_path.exists() and not Path(cfg.bootstrap_path).exists()
|
||||
assert cli.main(["-c", path, "bootstrap"]) == EX_OK
|
||||
assert "already bootstrapped" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_version_and_bad_config(capsys, tmp_path):
|
||||
assert cli.main(["version"]) == 0
|
||||
assert capsys.readouterr().out.strip() == "0.1.0"
|
||||
bad = tmp_path / "c.yaml"
|
||||
bad.write_text("env_id: nope\nsite: cbs\n")
|
||||
assert cli.main(["-c", str(bad), "status"]) == 78
|
||||
assert cli.main(["-c", str(tmp_path / "missing.yaml"), "status"]) == 1
|
||||
|
||||
|
||||
def test_sdk_transport_uses_openziti_monkeypatch(monkeypatch, tmp_path):
|
||||
"""The sdk transport loads the identity once and dials inside openziti.monkeypatch()."""
|
||||
import contextlib
|
||||
import socket
|
||||
import sys
|
||||
import types
|
||||
|
||||
calls = []
|
||||
fake = types.ModuleType("openziti")
|
||||
fake.load = lambda p: calls.append(("load", p)) or object()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mp():
|
||||
calls.append(("monkeypatch",))
|
||||
yield
|
||||
|
||||
fake.monkeypatch = mp
|
||||
monkeypatch.setitem(sys.modules, "openziti", fake)
|
||||
from monky_deployd.transport import SdkTransport
|
||||
|
||||
srv = socket.socket()
|
||||
srv.bind(("127.0.0.1", 0))
|
||||
srv.listen(1)
|
||||
t = SdkTransport(str(tmp_path / "id.json"))
|
||||
s = t.connect("127.0.0.1", srv.getsockname()[1], 2)
|
||||
s.close()
|
||||
srv.close()
|
||||
assert calls == [("load", str(tmp_path / "id.json")), ("monkeypatch",)]
|
||||
assert "sdk(identity=" in t.describe()
|
||||
|
||||
|
||||
def test_proxy_transport_refuses_unmapped_hosts():
|
||||
from monky_deployd.config import from_dict
|
||||
from monky_deployd.transport import TransportError, build
|
||||
|
||||
cfg = from_dict({"env_id": "env-dev-06", "site": "cbs", "transport": "proxy"})
|
||||
t = build(cfg)
|
||||
assert t.mapping[("monky.tenancy.deploy", 8081)] == ("127.0.0.1", 18443)
|
||||
assert t.mapping[("bao.cbs.tikali.net", 8200)] == ("127.0.0.1", 18200)
|
||||
try:
|
||||
t.connect("example.com", 443, 1)
|
||||
raise AssertionError("unmapped host must be refused")
|
||||
except TransportError:
|
||||
pass
|
||||
Reference in New Issue
Block a user