mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 04:36:15 +00:00
1c42e913a8
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
91 lines
5.1 KiB
Markdown
91 lines
5.1 KiB
Markdown
<!-- xlate:verbatim-fences -->
|
|
# monky-deployd — CLAUDE.md
|
|
|
|
## What this is
|
|
|
|
**monky-deployd** is the **on-box pull agent** for Monky backend environments on docker VMs and
|
|
laptops (MONKY-ADR-0028). It dials **monky-tenancy** over the OpenZiti mesh with the box's own host
|
|
identity, converges the box to the published bundle (`docker compose`), reads its secrets from
|
|
**OpenBao** itself, and reports — the check-in is also the liveness heartbeat. It is the apply half
|
|
of the backend lifecycle; the API/DB half is monky-tenancy (`/v1/backends*`, `/v1/agent/*`), the
|
|
composition source is monky-deploy (renderer), the console is monky-mgmt-ui. k8s backends have no
|
|
agent (ArgoCD applies).
|
|
|
|
## Vocabulary (hard rule)
|
|
|
|
**backend**, **environment** (`env_id` = `env-<tier>-<nn>`), **bundle**, **deploy grant**,
|
|
**check-in**. Check-in returns `action ∈ apply|none|down`; a report carries
|
|
`result ∈ applied|failed|down`. There is no `desired_sha="down"`, no "absent", no "up" result.
|
|
Never "tenant" (the API repo's name is the one exception).
|
|
|
|
## Non-negotiables
|
|
|
|
- **Stdlib only** in `monky_deployd/`. The only optional import is `openziti` (transport `sdk`),
|
|
loaded lazily inside `transport.SdkTransport`. No PyYAML: `config.parse_yaml_subset` reads the
|
|
config; keep the install kit / ansible template inside that subset.
|
|
- **The agent never receives a secret from tenancy** and **never logs a value**. Every value read
|
|
from OpenBao (and every token/grant) goes through `redact.REDACTOR.add()` the moment it exists;
|
|
refusals and reports name variables, never values. Tests assert the report tail carries none.
|
|
- **No AppRole, nothing to unwrap.** Auth is `POST /v1/auth/jwt-tenancy/login {"role":"see-env",
|
|
"jwt":<grant>}`; the resulting Bao token is the bearer to tenancy. An AppRole-shaped lease is a
|
|
loud `LEASE_SHAPE` failure, not a fallback.
|
|
- **Refuse before you lease.** All bundle checks (sha, env, unresolved vars, manifest paths,
|
|
privileged, rollback, disk) run before `POST /v1/agent/lease`, so a refused bundle costs no lease
|
|
(5/h budget) and no OpenBao login.
|
|
- **Exit codes are a contract** with the timer and the loop: 0 / 75 (temporary, retry) /
|
|
78 (`AGENT_ENV_MISMATCH`, never retried) / 1. `SuccessExitStatus=75` in the unit.
|
|
- **Prod never purges.** `-v` is dropped on prod even if tenancy asks; the agent never decides state.
|
|
- **Paths**: bearer at `/var/lib/monky-deployd/bao.token` (0600), grant at
|
|
`/etc/monky-deployd/bootstrap.jwt` (consumed then deleted/truncated), releases under
|
|
`<deploy_dir>/releases/<sha>` with a `current` symlink = the compose project directory.
|
|
|
|
## Layout
|
|
|
|
| module | role |
|
|
|---|---|
|
|
| `cli.py` | argparse, journald-friendly logging (`<N>` priority when `JOURNAL_STREAM`), `run/status/bootstrap/version` |
|
|
| `agent.py` | the tick (`Agent.run_once`), action dispatch, refusal checks, token upkeep, staging/promote |
|
|
| `config.py` | YAML-subset reader + `Config` dataclasses + validation |
|
|
| `transport.py` | `sdk` / `proxy` / `system` transports + `HttpClient` (http.client with TLS SNI) |
|
|
| `tenancy.py` | the four calls; error classes `Unauthenticated`, `EnvMismatch`, `RateLimited`, `LeaseShapeUnsupported` |
|
|
| `bao.py` | login, lookup/renew/revoke-self, KV-v2 read, `kv_data_path` (env pinning) |
|
|
| `bundle.py` | tar parse, `bundle_sha` (tenancy's formula), unresolved-var / privileged scans, `.env` rendering |
|
|
| `compose.py` | `docker compose` wrapper, `ps --format json` parsing (NDJSON + array), health wait |
|
|
| `state.py` | `state.json`, token file, atomic 0600 writes, `flock` |
|
|
| `redact.py` | the log filter |
|
|
|
|
## Tests
|
|
|
|
`pytest` is hermetic: `tests/fakes.py` runs a fake tenancy agent endpoint and a fake OpenBao as
|
|
real HTTP servers on loopback (shapes of record, incl. env pinning, superseded grants, rate
|
|
limits, the AppRole shape), and `tests/fakebin/docker` is a stub on `PATH` that records every
|
|
invocation. No network, no docker daemon, no root. Add a test for every new refusal code.
|
|
|
|
## Gates before you push
|
|
|
|
```sh
|
|
ruff format --check . || exit 1
|
|
ruff check . || exit 1
|
|
pytest || exit 1
|
|
bash -n packaging/install.sh || exit 1
|
|
systemd-analyze verify packaging/systemd/*.service # when available
|
|
```
|
|
|
|
## Releasing
|
|
|
|
Bump `monky_deployd.__version__` + `pyproject.toml` + `packaging/install.sh` `DEFAULT_VERSION` +
|
|
`CHANGELOG.md`, merge to `main`, tag `vX.Y.Z` (protected `v*`). The tag pipeline builds the `.deb`,
|
|
publishes the GitLab release and — with `GITEA_TOKEN` — the **Gitea release** the installer
|
|
downloads from (`docs/OPERATIONS.md` has the manual recipe). `lint` refuses a tag whose version
|
|
differs from `__version__`.
|
|
|
|
## What NOT to do
|
|
|
|
- Don't add a dependency to the agent; don't import `openziti` at module top level.
|
|
- Don't log, print or report a secret value, a token or a grant; don't put values in `state.json`.
|
|
- Don't add an AppRole/unwrap path "for compatibility".
|
|
- Don't apply when a check fails "with a warning"; refuse, report, exit 1.
|
|
- Don't retry `AGENT_ENV_MISMATCH`; don't turn 75 into a busy loop (the timer is the retry).
|
|
- Don't edit `*.ru.md` (doc-translator owns them); keep line 1 `<!-- xlate:verbatim-fences -->`.
|
|
- Cite decisions as `MONKY-ADR-NNNN`; don't restate them here.
|