mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 07:16:16 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35949a9246 | |||
| a30014a096 | |||
| 47c3840c95 | |||
| d0153dd71c | |||
| 6e8b2bf41d | |||
| d34189c625 | |||
| b749a6d3b4 | |||
| 05724edde2 | |||
| b39a60af46 | |||
| fcf2dcb1eb | |||
| 31586c3058 |
@@ -1,6 +1,30 @@
|
|||||||
<!-- xlate:verbatim-fences -->
|
<!-- xlate:verbatim-fences -->
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.6 — 2026-09-07
|
||||||
|
|
||||||
|
- **A valid lease token is reused across applies.** Every apply requested a new lease, so a failing deploy
|
||||||
|
retried by the 60 s timer burned tenancy's 5-leases-per-hour budget and then failed on
|
||||||
|
`LEASE_RATE_LIMITED` forever (env-qa-02 pilot). Now: reuse a lease-derived token while `lookup-self`
|
||||||
|
says it is valid; swap the bootstrap token for a lease once; and if tenancy rate-limits the lease while a
|
||||||
|
working token exists, apply with it and defer the swap instead of failing the deploy.
|
||||||
|
|
||||||
|
## 0.1.5 — 2026-09-07
|
||||||
|
|
||||||
|
- **`tenancy.port` defaults to 443 everywhere** — the ansible role default and the `TenancyCfg` default now
|
||||||
|
match `install.sh`/`config.example.yaml` (0.1.5 only changed those two); PROTOCOL.md states the intercept
|
||||||
|
port separately from the in-pod 8081, that the broker does not add `#monky-deploy-agent` yet, that the jwt
|
||||||
|
mount keeps no `jti` state, and that `release:gitea` has not run (doc-drift DD-0523/0525/0527/0528).
|
||||||
|
- **`${VAR}` inside comment lines is not a reference.** The renderer's `.env.template` header literally says
|
||||||
|
"substitutes every ${VAR}", which the refusal check counted as an unresolved variable
|
||||||
|
(`ENV_INCOMPLETE: unresolved: VAR`) — the first bundle on env-qa-02 was refused for it.
|
||||||
|
|
||||||
|
- **config: `tenancy.port` is the service's intercept port (443), not the in-pod 8081.** With 8081 the SDK
|
||||||
|
found no intercept and the dial failed (`service not available`, then a bare `TypeError` from the SDK's
|
||||||
|
fallback). `install.sh` now writes 443; `config.example.yaml` updated.
|
||||||
|
- **transport sdk: clear error instead of a TypeError** when an address has no intercept or the identity
|
||||||
|
has no dial policy for the service (`TransportError` names the host:port and what to check).
|
||||||
|
|
||||||
## 0.1.4 — 2026-09-07
|
## 0.1.4 — 2026-09-07
|
||||||
|
|
||||||
- **install.sh: `ziti-edge-tunnel.service` failed to start after enrolment** (`203/EXEC`: the package's
|
- **install.sh: `ziti-edge-tunnel.service` failed to start after enrolment** (`203/EXEC`: the package's
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ you the enrolment JWT, a one-time **bootstrap deploy grant** and the read-only *
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
T=<deploy token> # read-only GitLab deploy token (read_package_registry); the kit carries it
|
T=<deploy token> # read-only GitLab deploy token (read_package_registry); the kit carries it
|
||||||
curl -sSf -H "DEPLOY-TOKEN: $T" https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.4/install.sh \
|
curl -sSf -H "DEPLOY-TOKEN: $T" https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.6/install.sh \
|
||||||
| sudo bash -s -- --env env-qa-02 --site cbs --token "$T" --bootstrap-file bootstrap.jwt --enrol-jwt ./monky-host.env-qa-02.jwt
|
| sudo bash -s -- --env env-qa-02 --site cbs --token "$T" --bootstrap-file bootstrap.jwt --enrol-jwt ./monky-host.env-qa-02.jwt
|
||||||
# [--transport sdk|proxy|system] [--version 0.1.4] [--docker-data-root /home/docker-data] [--laptop] [--bao-ca openbao-ca.pem] [--source gitlab|gitea]
|
# [--transport sdk|proxy|system] [--version 0.1.6] [--docker-data-root /home/docker-data] [--laptop] [--bao-ca openbao-ca.pem] [--source gitlab|gitea]
|
||||||
```
|
```
|
||||||
|
|
||||||
`install.sh` installs `ziti-edge-tunnel` (OpenZiti `jammy` suite) and `docker-compose-plugin` if
|
`install.sh` installs `ziti-edge-tunnel` (OpenZiti `jammy` suite) and `docker-compose-plugin` if
|
||||||
@@ -98,11 +98,13 @@ monky-deployd version
|
|||||||
## Security model
|
## Security model
|
||||||
|
|
||||||
- **Identity = the box's ziti host identity.** Only identities with `#monky-deploy-agent` can dial
|
- **Identity = the box's ziti host identity.** Only identities with `#monky-deploy-agent` can dial
|
||||||
tenancy's agent entrypoint; `#openbao-client` reaches OpenBao. The agent reads the identity
|
tenancy's agent entrypoint; `#openbao-client` reaches OpenBao — tenancy sends both to the broker
|
||||||
|
as `host_extra_attrs` at register (monky-tenancy !29 / monky-ziti `ac258352`, 2026-09-07), so a
|
||||||
|
backend registered since then dials without hand-added attrs (see PROTOCOL.md §Where and how). The agent reads the identity
|
||||||
through an ACL (`setfacl -m u:monky-deployd:r`), never owns it.
|
through an ACL (`setfacl -m u:monky-deployd:r`), never owns it.
|
||||||
- **The bearer to tenancy is the agent's own OpenBao token**, minted by OpenBao from a
|
- **The bearer to tenancy is the agent's own OpenBao token**, minted by OpenBao from a
|
||||||
tenancy-signed ES256 deploy grant (`aud openbao-see-env`, `kind deploy-grant`, 1 h, single-use
|
tenancy-signed ES256 deploy grant (`aud openbao-see-env`, `kind deploy-grant`, 1 h; its `jti`
|
||||||
`jti`). Tenancy verifies it with `auth/token/lookup`, pins `meta.env_id`, and refuses a token
|
becomes the token's `meta.grant_jti` — the mount keeps no replay state, tenancy does). Tenancy verifies it with `auth/token/lookup`, pins `meta.env_id`, and refuses a token
|
||||||
whose `meta.grant_jti` was superseded (kit re-reveal, retire) → `401 AGENT_UNAUTHENTICATED`.
|
whose `meta.grant_jti` was superseded (kit re-reveal, retire) → `401 AGENT_UNAUTHENTICATED`.
|
||||||
**No AppRole, nothing to unwrap** (Gate 1 result, 2026-09-05).
|
**No AppRole, nothing to unwrap** (Gate 1 result, 2026-09-05).
|
||||||
- **The agent never receives a secret from tenancy.** Bundles carry placeholders; the agent reads
|
- **The agent never receives a secret from tenancy.** Bundles carry placeholders; the agent reads
|
||||||
@@ -160,7 +162,9 @@ blocking on `main`/tags (manual on MRs); the `.deb` always ships the SDK wheel.
|
|||||||
`release` uploads to the GitLab generic package registry + release (**the primary download**; the
|
`release` uploads to the GitLab generic package registry + release (**the primary download**; the
|
||||||
project is private, so the installer sends the read-only deploy token), and `release:gitea` publishes the same assets on the Gitea mirror (the `--source gitea`
|
project is private, so the installer sends the read-only deploy token), and `release:gitea` publishes the same assets on the Gitea mirror (the `--source gitea`
|
||||||
alternative; automatic when `GITEA_TOKEN` is set, manual otherwise — see `docs/OPERATIONS.md` for
|
alternative; automatic when `GITEA_TOKEN` is set, manual otherwise — see `docs/OPERATIONS.md` for
|
||||||
the by-hand recipe). Both locations keep being published.
|
the by-hand recipe). The GitLab registry is published by every tag pipeline; the Gitea mirror only
|
||||||
|
when `release:gitea` runs — automatically once `GITEA_TOKEN` is set in CI, by hand otherwise — so
|
||||||
|
check the Gitea release page before pointing an installer at it.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ template → optional openbao-ca PEM → optional `monky-deployd-proxy.service`
|
|||||||
|
|
||||||
| var | note |
|
| var | note |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `monky_deployd_version` | pinned release, e.g. `0.1.4` |
|
| `monky_deployd_version` | pinned release, e.g. `0.1.6` |
|
||||||
| `monky_deployd_download_token` | **vaulted**: GitLab deploy token, scope `read_package_registry` only (revocable) — the registry is private; seeded in OpenBao at `monky/monky-tenancy/deployd-download` key `token` (path/key are the operator's choice). Empty = no header (only works with the Gitea `base_url`) |
|
| `monky_deployd_download_token` | **vaulted**: GitLab deploy token, scope `read_package_registry` only (revocable) — the registry is private; seeded in OpenBao at `monky/monky-tenancy/deployd-download` key `token` (path/key are the operator's choice). Empty = no header (only works with the Gitea `base_url`) |
|
||||||
| `monky_deployd_env_id` / `_site` / `_transport` | per host (`env-dev-06`, `cbs`, `sdk`) |
|
| `monky_deployd_env_id` / `_site` / `_transport` | per host (`env-dev-06`, `cbs`, `sdk`) |
|
||||||
| `monky_deployd_bootstrap_grant` | tenancy-minted deploy grant (1 h) — `ansible-vault` or a lookup at play time; empty keeps the existing token |
|
| `monky_deployd_bootstrap_grant` | tenancy-minted deploy grant (1 h) — `ansible-vault` or a lookup at play time; empty keeps the existing token |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
# monky_deployd — install and configure the Monky backend pull agent (MONKY-ADR-0028 §D).
|
# monky_deployd — install and configure the Monky backend pull agent (MONKY-ADR-0028 §D).
|
||||||
# Copy this role into osg1-07 (roles/monky_deployd) and roll to env-dev-06..09 after the pilot.
|
# Copy this role into osg1-07 (roles/monky_deployd) and roll to env-dev-06..09 after the pilot.
|
||||||
monky_deployd_version: "0.1.4"
|
monky_deployd_version: "0.1.6"
|
||||||
monky_deployd_deb: "monky-deployd_{{ monky_deployd_version }}_amd64.deb"
|
monky_deployd_deb: "monky-deployd_{{ monky_deployd_version }}_amd64.deb"
|
||||||
# PRIMARY download = the GitLab project's generic package registry on scm.tikali.ai. Inside the estate
|
# PRIMARY download = the GitLab project's generic package registry on scm.tikali.ai. Inside the estate
|
||||||
# gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175, no HTTP ingress), so backend
|
# gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175, no HTTP ingress), so backend
|
||||||
@@ -33,7 +33,7 @@ monky_deployd_disk_factor: 1.5
|
|||||||
monky_deployd_disk_headroom_bytes: 2147483648
|
monky_deployd_disk_headroom_bytes: 2147483648
|
||||||
|
|
||||||
monky_deployd_tenancy_service: monky.tenancy.deploy
|
monky_deployd_tenancy_service: monky.tenancy.deploy
|
||||||
monky_deployd_tenancy_port: 8081
|
monky_deployd_tenancy_port: 443 # the intercept port of monky.tenancy.deploy (host.v1 forwards to 8081 in the pod)
|
||||||
monky_deployd_bao_addr: "https://bao.cbs.tikali.net:8200"
|
monky_deployd_bao_addr: "https://bao.cbs.tikali.net:8200"
|
||||||
monky_deployd_bao_auth_mount: jwt-tenancy
|
monky_deployd_bao_auth_mount: jwt-tenancy
|
||||||
monky_deployd_bao_role: see-env
|
monky_deployd_bao_role: see-env
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# /etc/monky-deployd/config.yaml — monky-deployd v0.1.4 (MONKY-ADR-0028 §D)
|
# /etc/monky-deployd/config.yaml — monky-deployd v0.1.6 (MONKY-ADR-0028 §D)
|
||||||
# Written by packaging/install.sh (or the ansible role monky_deployd). YAML *subset*: maps, scalars,
|
# Written by packaging/install.sh (or the ansible role monky_deployd). YAML *subset*: maps, scalars,
|
||||||
# simple lists, comments. Keys not listed here are a config error.
|
# simple lists, comments. Keys not listed here are a config error.
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json # the box's h
|
|||||||
tenancy:
|
tenancy:
|
||||||
service: monky.tenancy.deploy # ziti service bound by the tenancy sidecar -> 127.0.0.1:8081 (agent entrypoint)
|
service: monky.tenancy.deploy # ziti service bound by the tenancy sidecar -> 127.0.0.1:8081 (agent entrypoint)
|
||||||
host: monky.tenancy.deploy # intercept host (sdk/system); defaults to `service`
|
host: monky.tenancy.deploy # intercept host (sdk/system); defaults to `service`
|
||||||
port: 8081
|
port: 443 # intercept port of monky.tenancy.deploy (in-pod 8081)
|
||||||
scheme: http # plain HTTP inside the mesh; the mesh is the transport security
|
scheme: http # plain HTTP inside the mesh; the mesh is the transport security
|
||||||
proxy_addr: 127.0.0.1:18443 # transport: proxy
|
proxy_addr: 127.0.0.1:18443 # transport: proxy
|
||||||
timeout_s: 30
|
timeout_s: 30
|
||||||
|
|||||||
+2
-1
@@ -108,7 +108,8 @@ download; xtrace is switched off). A 401 on the download means the token is miss
|
|||||||
lacks the scope. Inside the estate `gitea.cbs.tikali.net` is split-horizon to jump1's RED EIP
|
lacks the scope. Inside the estate `gitea.cbs.tikali.net` is split-horizon to jump1's RED EIP
|
||||||
(`10.10.0.175`), which has no HTTP ingress, so backend boxes cannot fetch from the Gitea mirror
|
(`10.10.0.175`), which has no HTTP ingress, so backend boxes cannot fetch from the Gitea mirror
|
||||||
(cbs/iac#102). Off-estate, `install.sh --source gitea` (ansible: `monky_deployd_base_url`/`_deb_url`,
|
(cbs/iac#102). Off-estate, `install.sh --source gitea` (ansible: `monky_deployd_base_url`/`_deb_url`,
|
||||||
no token) uses the Gitea release instead. The tag pipeline publishes to both (`release`, `release:gitea`).
|
no token) uses the Gitea release instead. The tag pipeline always publishes to GitLab (`release`); `release:gitea` is automatic only with
|
||||||
|
`GITEA_TOKEN` in CI and a manual job otherwise — it has not run on a tag pipeline yet.
|
||||||
|
|
||||||
## Publishing a release to Gitea by hand
|
## Publishing a release to Gitea by hand
|
||||||
|
|
||||||
|
|||||||
+36
-19
@@ -10,8 +10,15 @@ The agent side of the backend lifecycle agent protocol — [MONKY-ADR-0028](http
|
|||||||
|
|
||||||
- **Where:** tenancy's **agent entrypoint** — a second container (`app.agent_main`,
|
- **Where:** tenancy's **agent entrypoint** — a second container (`app.agent_main`,
|
||||||
`127.0.0.1:8081`) bound to the ziti service **`monky.tenancy.deploy`** by a `ziti-edge-tunnel
|
`127.0.0.1:8081`) bound to the ziti service **`monky.tenancy.deploy`** by a `ziti-edge-tunnel
|
||||||
run-host` sidecar. Unreachable from the public ingress. Only host identities carrying
|
run-host` sidecar. Unreachable from the public ingress. **The agent dials the service's
|
||||||
`#monky-deploy-agent` may dial (the broker adds the attr when the identity is created at kit reveal).
|
INTERCEPT port, `tcp/443`** (`monky.tenancy.deploy.intercept.v1` in openziti
|
||||||
|
`state/overlay/configs.json` — the authority for the port); the sidecar's `host.v1` forwards to
|
||||||
|
`127.0.0.1:8081` inside the pod. `tenancy.port` in the config, `install.sh` and the ansible role all
|
||||||
|
default to 443 (0.1.5). Only host identities carrying `#monky-deploy-agent` may dial
|
||||||
|
(`monky.tenancy.deploy.dial`, openziti). Tenancy sends that attr and `#openbao-client` to the
|
||||||
|
broker as `host_extra_attrs` when it registers an agent-backed backend (monky-tenancy !29,
|
||||||
|
monky-ziti `ac258352` — both 2026-09-07), so `monky-host.<env>` carries them from creation;
|
||||||
|
identities created before that (env-qa-02's) had them added on the controller by hand.
|
||||||
- **Transport:** plain HTTP inside the mesh (`transport: sdk` dials by service name;
|
- **Transport:** plain HTTP inside the mesh (`transport: sdk` dials by service name;
|
||||||
`proxy` → `127.0.0.1:18443`). OpenBao is the existing `openbao` ziti service (`#openbao-client`),
|
`proxy` → `127.0.0.1:18443`). OpenBao is the existing `openbao` ziti service (`#openbao-client`),
|
||||||
dialled as `https://bao.cbs.tikali.net:8200` (an intercept name), TLS validated against the
|
dialled as `https://bao.cbs.tikali.net:8200` (an intercept name), TLS validated against the
|
||||||
@@ -29,7 +36,7 @@ The agent side of the backend lifecycle agent protocol — [MONKY-ADR-0028](http
|
|||||||
```
|
```
|
||||||
kit reveal (admin, once) tenancy signs a bootstrap DEPLOY GRANT: ES256 JWT
|
kit reveal (admin, once) tenancy signs a bootstrap DEPLOY GRANT: ES256 JWT
|
||||||
iss <tenancy issuer>, aud openbao-see-env, sub agent:<env_id>,
|
iss <tenancy issuer>, aud openbao-see-env, sub agent:<env_id>,
|
||||||
env_id, kind deploy-grant, jti (single-use, recorded), exp now+1h
|
env_id, kind deploy-grant, jti (→ token meta grant_jti; tenancy records it), exp now+1h
|
||||||
install.sh stages it at /etc/monky-deployd/bootstrap.jwt (0600)
|
install.sh stages it at /etc/monky-deployd/bootstrap.jwt (0600)
|
||||||
first tick POST https://bao…/v1/auth/jwt-tenancy/login {"role":"see-env","jwt":<grant>}
|
first tick POST https://bao…/v1/auth/jwt-tenancy/login {"role":"see-env","jwt":<grant>}
|
||||||
→ auth.client_token (ttl 24 h, max 30 d, policy see-env,
|
→ auth.client_token (ttl 24 h, max 30 d, policy see-env,
|
||||||
@@ -49,10 +56,13 @@ OpenBao side (Terraform, `tikali/services/security/openbao`): mount `jwt-tenancy
|
|||||||
`monky/data/{{identity.entity.aliases.<accessor>.metadata.env_id}}/see/*` read + metadata list +
|
`monky/data/{{identity.entity.aliases.<accessor>.metadata.env_id}}/see/*` read + metadata list +
|
||||||
`auth/token/{renew-self,lookup-self}`. **Gate 1 v2 passed 2026-09-05 07:40Z** (distinct entity per env).
|
`auth/token/{renew-self,lookup-self}`. **Gate 1 v2 passed 2026-09-05 07:40Z** (distinct entity per env).
|
||||||
|
|
||||||
**Second-reveal semantics.** A kit re-reveal (or a retire) supersedes every earlier grant of the
|
**Second-reveal semantics.** A kit re-reveal (or a retire) supersedes every earlier bootstrap grant
|
||||||
backend and revokes the token accessors tenancy knows. The old kit's grant fails at login
|
of the backend (tenancy marks their `backend_leases` rows revoked) and revokes the token accessors
|
||||||
(unknown/used `jti`), and a token already minted from it is refused at the next call with
|
tenancy knows. The `jwt-tenancy` mount keeps **no** replay state — it validates signature, `aud`,
|
||||||
`401 AGENT_UNAUTHENTICATED` (its `meta.grant_jti` is superseded) — **not** `AGENT_ENV_MISMATCH`.
|
`bound_claims` and `exp` only (openbao `terraform/jwt-tenancy.tf`, the `see_env` role) — so a
|
||||||
|
superseded grant still logs in until its 1 h `exp`; the token it yields, like a token already minted
|
||||||
|
from it, is refused by tenancy at the first bearer call with `401 AGENT_UNAUTHENTICATED` (its
|
||||||
|
`meta.grant_jti` is superseded) — **not** `AGENT_ENV_MISMATCH`.
|
||||||
The agent then deletes its token; if a fresh `bootstrap.jwt` is on disk it bootstraps again in the
|
The agent then deletes its token; if a fresh `bootstrap.jwt` is on disk it bootstraps again in the
|
||||||
same tick, otherwise it exits 1 and says "re-run the install kit".
|
same tick, otherwise it exits 1 and says "re-run the install kit".
|
||||||
|
|
||||||
@@ -70,7 +80,8 @@ same tick, otherwise it exits 1 and says "re-run the install kit".
|
|||||||
```json
|
```json
|
||||||
{"env_id": "env-qa-02", "desired_sha": "7a10…", "action": "apply", "purge_volumes": false,
|
{"env_id": "env-qa-02", "desired_sha": "7a10…", "action": "apply", "purge_volumes": false,
|
||||||
"bundle_url": "/v1/agent/bundle/env-qa-02/7a10…", "checkin_interval_s": 60,
|
"bundle_url": "/v1/agent/bundle/env-qa-02/7a10…", "checkin_interval_s": 60,
|
||||||
"vault": {"addr": "https://bao.cbs.tikali.net:8200", "mount": "monky", "prefix": "env-qa-02/see"}}
|
"vault": {"addr": "https://bao.cbs.tikali.net:8200", "mount": "monky", "prefix": "env-qa-02/see",
|
||||||
|
"auth_mount": "jwt-tenancy", "auth_role": "see-env"}}
|
||||||
```
|
```
|
||||||
`action`: `apply` (desired ≠ applied), `none` (converged → heartbeat), `down` (retire; `purge_volumes`
|
`action`: `apply` (desired ≠ applied), `none` (converged → heartbeat), `down` (retire; `purge_volumes`
|
||||||
is only meaningful here). `vault.mount` is the **KV** mount; the agent adopts it if it differs from
|
is only meaningful here). `vault.mount` is the **KV** mount; the agent adopts it if it differs from
|
||||||
@@ -109,12 +120,16 @@ address, pull, renderer, images_policy, secrets_provider, agent{…}, files[]`.
|
|||||||
### `POST /v1/agent/lease`
|
### `POST /v1/agent/lease`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"env_id": "env-qa-02", "reason": "apply"} // reason: apply | renew
|
{"env_id": "env-qa-02"}
|
||||||
```
|
```
|
||||||
|
(The agent also sends `"reason": "apply" | "renew"` for its own logs; tenancy's `AgentLease` is
|
||||||
|
`{env_id}` and ignores unknown fields.)
|
||||||
```json
|
```json
|
||||||
{"env_id": "env-qa-02", "login_jwt": "eyJ…", "ttl_s": 3600, "mount": "jwt-tenancy", "role": "see-env",
|
{"env_id": "env-qa-02", "login_jwt": "eyJ…", "ttl_s": 3600, "mount": "jwt-tenancy", "role": "see-env",
|
||||||
"vault": {"addr": "https://bao.cbs.tikali.net:8200", "mount": "monky"}}
|
"addr": "https://bao.cbs.tikali.net:8200"}
|
||||||
```
|
```
|
||||||
|
`addr` is the OpenBao address for the login below, top-level (tenancy `AgentLeaseOut`); the KV
|
||||||
|
mount/prefix come from `checkin`'s `vault`, not from the lease.
|
||||||
Then `POST /v1/auth/{mount}/login {"role": "{role}", "jwt": "{login_jwt}"}` on OpenBao. A body with
|
Then `POST /v1/auth/{mount}/login {"role": "{role}", "jwt": "{login_jwt}"}` on OpenBao. A body with
|
||||||
`wrapping_token` / `role_id` (the pre-Gate-1 AppRole lease) is refused with `LEASE_SHAPE` → report
|
`wrapping_token` / `role_id` (the pre-Gate-1 AppRole lease) is refused with `LEASE_SHAPE` → report
|
||||||
`failed`. `429 LEASE_RATE_LIMITED` → exit 75.
|
`failed`. `429 LEASE_RATE_LIMITED` → exit 75.
|
||||||
@@ -133,15 +148,17 @@ first 2 KiB land in the audit log — it has been through the redactor.
|
|||||||
|
|
||||||
## Divergences (2026-09-05)
|
## Divergences (2026-09-05)
|
||||||
|
|
||||||
- **monky-tenancy `main` (MR !15) still implements the AppRole lease and install kit**
|
- **Resolved 2026-09-05 (before v0.1.0 was tagged):** monky-tenancy !17 (`288df791`) landed the
|
||||||
(`AgentLeaseOut{wrapping_token, role_id}`, `bootstrap.wrap`, `bao.approle` in the kit's config). The
|
tenancy side of the Gate 1 RESULT / ADR-0028 amendment — `AgentLeaseOut{env_id, login_jwt, ttl_s,
|
||||||
binding design is the plan's Gate 1 RESULT / ADR-0028 amendment: `{login_jwt, ttl_s, mount, role}`
|
mount, role, addr}`, the ES256 deploy-grant signer (`app/agent_keys.py`), `GET
|
||||||
and `POST /v1/auth/jwt-tenancy/login`. This agent implements the latter; against an un-migrated
|
/.well-known/agent-jwks.json`, no AppRole and no response wrapping anywhere in tenancy. The
|
||||||
tenancy it reports `failed` with `LEASE_SHAPE` and refuses `bao.approle` in its config. The tenancy
|
`LEASE_SHAPE` refusal of a `wrapping_token`/`role_id` body stays in this agent as a guard against a
|
||||||
follow-up (deploy-grant signer, JWKS, `lease` shape, kit → `bootstrap.jwt`) is tracked on
|
stale tenancy, not as a description of `main`.
|
||||||
monky-tenancy.
|
- **Resolved 2026-09-05:** the kit no longer generates a config file. Since monky-tenancy !22
|
||||||
- The kit's generated config uses `tenancy.base_url: http://monky.tenancy.deploy:8081` — accepted as
|
(`61bd0281`) the one-time install script stages the bootstrap grant and runs `install.sh --env …
|
||||||
an alias for `tenancy.{scheme,host,port}`.
|
--site … --version … --bootstrap-file …`, and `install.sh` writes `/etc/monky-deployd/config.yaml`
|
||||||
|
(tenancy service name, OpenBao address and `jwt-tenancy`/`see-env`). `tenancy.base_url:` remains an
|
||||||
|
accepted alias for `tenancy.{scheme,host,port}` (`config.py`) for hand-written configs.
|
||||||
- `report` gains an optional `detail` (doc 24 §3.3); tenancy's `AgentReport` ignores unknown fields
|
- `report` gains an optional `detail` (doc 24 §3.3); tenancy's `AgentReport` ignores unknown fields
|
||||||
today — if `strict` bodies land, `detail` folds into `log_tail`.
|
today — if `strict` bodies land, `detail` folds into `log_tail`.
|
||||||
- Bundle sha header: tenancy sends `X-Bundle-Sha`, doc 24 says `X-Bundle-Sha256`; the agent reads
|
- Bundle sha header: tenancy sends `X-Bundle-Sha`, doc 24 says `X-Bundle-Sha256`; the agent reads
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ Dials monky-tenancy over the mesh with the box's host identity, fetches the rend
|
|||||||
leases a deploy grant, logs in to OpenBao, reads its own secrets, runs `docker compose`,
|
leases a deploy grant, logs in to OpenBao, reads its own secrets, runs `docker compose`,
|
||||||
reports. Stdlib only; the optional `openziti` SDK is the `sdk` transport."""
|
reports. Stdlib only; the optional `openziti` SDK is the `sdk` transport."""
|
||||||
|
|
||||||
__version__ = "0.1.4"
|
__version__ = "0.1.6"
|
||||||
|
|||||||
+24
-1
@@ -277,7 +277,7 @@ class Agent:
|
|||||||
# secrets: lease -> login -> reads (values never logged; names only)
|
# secrets: lease -> login -> reads (values never logged; names only)
|
||||||
entries = b.manifest.get("entries", [])
|
entries = b.manifest.get("entries", [])
|
||||||
if entries:
|
if entries:
|
||||||
token = self._lease_login("apply")
|
token = self._token_for_apply()
|
||||||
for e in entries:
|
for e in entries:
|
||||||
self._values[e["var"]] = self.bao.kv_read(token, e["path"], cfg.env_id, e.get("version"))
|
self._values[e["var"]] = self.bao.kv_read(token, e["path"], cfg.env_id, e.get("version"))
|
||||||
log.info("read %d secret(s): %s", len(entries), ", ".join(sorted(self._values)))
|
log.info("read %d secret(s): %s", len(entries), ", ".join(sorted(self._values)))
|
||||||
@@ -392,6 +392,29 @@ class Agent:
|
|||||||
if old and old != token:
|
if old and old != token:
|
||||||
self.bao.revoke_self(old)
|
self.bao.revoke_self(old)
|
||||||
|
|
||||||
|
def _token_for_apply(self) -> str:
|
||||||
|
"""The OpenBao token to read this bundle's secrets with. A lease-derived token that is
|
||||||
|
still valid is REUSED (a lease per apply burned tenancy's 5/h budget on every retry —
|
||||||
|
env-qa-02 pilot, 2026-09-07); the bootstrap token is swapped for a lease once; and if
|
||||||
|
tenancy rate-limits the lease while we hold a working token, apply with what we have
|
||||||
|
and swap later rather than fail the deploy."""
|
||||||
|
cfg = self.cfg
|
||||||
|
st = self.state.token
|
||||||
|
if self.token and st is not None and st.source == "lease":
|
||||||
|
try:
|
||||||
|
info = self.bao.lookup_self(self.token)
|
||||||
|
if int(info.get("ttl") or 0) > cfg.bao.renew_below_s:
|
||||||
|
return self.token
|
||||||
|
except BaoError as exc:
|
||||||
|
log.info("lease token no longer valid (%s); re-leasing", exc)
|
||||||
|
try:
|
||||||
|
return self._lease_login("apply")
|
||||||
|
except RateLimited as exc:
|
||||||
|
if self.token:
|
||||||
|
log.warning("lease rate-limited (%s); applying with the current token, swap deferred", exc)
|
||||||
|
return self.token
|
||||||
|
raise
|
||||||
|
|
||||||
def _lease_login(self, reason: str) -> str:
|
def _lease_login(self, reason: str) -> str:
|
||||||
assert self.tenancy is not None
|
assert self.tenancy is not None
|
||||||
lease = self.tenancy.lease(reason)
|
lease = self.tenancy.lease(reason)
|
||||||
|
|||||||
@@ -144,12 +144,18 @@ def parse(data: bytes, *, max_bytes: int = 4 * 1024 * 1024) -> Bundle:
|
|||||||
# --- refusal checks (pure; names only, never values) -----------------------------------------
|
# --- refusal checks (pure; names only, never values) -----------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _code_lines(text: str) -> str:
|
||||||
|
"""Drop comment lines: a `# … ${VAR} …` remark in .env.template (the renderer writes one)
|
||||||
|
is not a reference. Compose/dotenv comments start with `#` after optional whitespace."""
|
||||||
|
return "\n".join(ln for ln in text.splitlines() if not ln.lstrip().startswith("#"))
|
||||||
|
|
||||||
|
|
||||||
def referenced_vars(text: str) -> set[str]:
|
def referenced_vars(text: str) -> set[str]:
|
||||||
return {m.group(1) for m in _VAR_RE.finditer(text)}
|
return {m.group(1) for m in _VAR_RE.finditer(_code_lines(text))}
|
||||||
|
|
||||||
|
|
||||||
def defaulted_vars(text: str) -> set[str]:
|
def defaulted_vars(text: str) -> set[str]:
|
||||||
return {m.group(1) for m in _VAR_DEFAULTED_RE.finditer(text)}
|
return {m.group(1) for m in _VAR_DEFAULTED_RE.finditer(_code_lines(text))}
|
||||||
|
|
||||||
|
|
||||||
def unresolved_vars(bundle: Bundle, provided: set[str]) -> list[str]:
|
def unresolved_vars(bundle: Bundle, provided: set[str]) -> list[str]:
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def parse_yaml_subset(text: str) -> dict:
|
|||||||
class TenancyCfg:
|
class TenancyCfg:
|
||||||
service: str = "monky.tenancy.deploy"
|
service: str = "monky.tenancy.deploy"
|
||||||
host: str = "" # intercept host; defaults to `service`
|
host: str = "" # intercept host; defaults to `service`
|
||||||
port: int = 8081
|
port: int = 443 # the service's INTERCEPT port (openziti); the pod-side listener is 127.0.0.1:8081
|
||||||
scheme: str = "http"
|
scheme: str = "http"
|
||||||
proxy_addr: str = "127.0.0.1:18443"
|
proxy_addr: str = "127.0.0.1:18443"
|
||||||
timeout_s: int = 30
|
timeout_s: int = 30
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Bearer = the agent's OpenBao token (from the `jwt-tenancy` login). Tenancy pins
|
|||||||
the token's `meta.env_id` (403 AGENT_ENV_MISMATCH -> exit 78, never retried) and refuses a token
|
the token's `meta.env_id` (403 AGENT_ENV_MISMATCH -> exit 78, never retried) and refuses a token
|
||||||
whose deploy grant was superseded (401 AGENT_UNAUTHENTICATED -> re-bootstrap or re-run the kit).
|
whose deploy grant was superseded (401 AGENT_UNAUTHENTICATED -> re-bootstrap or re-run the kit).
|
||||||
|
|
||||||
Lease shape of record (Gate 1 v2, 2026-09-05): `{login_jwt, ttl_s, mount, role, vault}`. An
|
Lease shape of record (Gate 1 v2, 2026-09-05): `{env_id, login_jwt, ttl_s, mount, role, addr}`. An
|
||||||
AppRole-era body (`wrapping_token`, `role_id`) is refused loudly — there is nothing to unwrap."""
|
AppRole-era body (`wrapping_token`, `role_id`) is refused loudly — there is nothing to unwrap."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -158,13 +158,15 @@ class TenancyClient:
|
|||||||
"(ADR-0028 amendment 2026-09-05)",
|
"(ADR-0028 amendment 2026-09-05)",
|
||||||
)
|
)
|
||||||
raise TenancyError(200, "LEASE_SHAPE", "lease response carries no login_jwt")
|
raise TenancyError(200, "LEASE_SHAPE", "lease response carries no login_jwt")
|
||||||
|
# shape of record (tenancy AgentLeaseOut): `addr` is top-level; a pre-0.1.x `vault{}` object
|
||||||
|
# is still read as a fallback so an older fake or tenancy does not break the lease
|
||||||
v = js.get("vault") or {}
|
v = js.get("vault") or {}
|
||||||
return Lease(
|
return Lease(
|
||||||
login_jwt=str(js["login_jwt"]),
|
login_jwt=str(js["login_jwt"]),
|
||||||
ttl_s=int(js.get("ttl_s") or 3600),
|
ttl_s=int(js.get("ttl_s") or 3600),
|
||||||
mount=str(js.get("mount") or "jwt-tenancy"),
|
mount=str(js.get("mount") or "jwt-tenancy"),
|
||||||
role=str(js.get("role") or "see-env"),
|
role=str(js.get("role") or "see-env"),
|
||||||
vault=Vault(addr=v.get("addr"), mount=v.get("mount"), prefix=v.get("prefix")),
|
vault=Vault(addr=js.get("addr") or v.get("addr"), mount=v.get("mount"), prefix=v.get("prefix")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def report(
|
def report(
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ class SdkTransport(Transport):
|
|||||||
return socket.create_connection((host, port), timeout=timeout)
|
return socket.create_connection((host, port), timeout=timeout)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise TransportError(f"transport sdk: dial {host}:{port} failed: {exc}") from exc
|
raise TransportError(f"transport sdk: dial {host}:{port} failed: {exc}") from exc
|
||||||
|
except Exception as exc: # noqa: BLE001 - the SDK raises bare Exception((code, msg)) and TypeError
|
||||||
|
# openziti-sdk-py: an address with NO matching intercept falls through to
|
||||||
|
# PySocket.connect(tuple) → TypeError; a matching intercept the identity may not dial
|
||||||
|
# raises Exception((-18, 'service not available')) — env-qa-02 pilot, 2026-09-07.
|
||||||
|
raise TransportError(
|
||||||
|
f"transport sdk: dial {host}:{port} failed: {exc} — no intercept for that host:port, or this "
|
||||||
|
"identity has no dial policy for the service (check the intercept port and the identity's attrs)"
|
||||||
|
) from exc
|
||||||
|
|
||||||
def describe(self) -> str:
|
def describe(self) -> str:
|
||||||
return f"sdk(identity={self.identity_path})"
|
return f"sdk(identity={self.identity_path})"
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
#
|
#
|
||||||
# T=<read-only GitLab deploy token, scope read_package_registry> # from the install kit / OpenBao
|
# T=<read-only GitLab deploy token, scope read_package_registry> # from the install kit / OpenBao
|
||||||
# curl -sSf -H "DEPLOY-TOKEN: $T" \
|
# curl -sSf -H "DEPLOY-TOKEN: $T" \
|
||||||
# https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.4/install.sh \
|
# https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.6/install.sh \
|
||||||
# | sudo bash -s -- --env env-qa-02 --site cbs --token "$T" --bootstrap-file bootstrap.jwt \
|
# | sudo bash -s -- --env env-qa-02 --site cbs --token "$T" --bootstrap-file bootstrap.jwt \
|
||||||
# [--transport sdk|proxy|system] [--version 0.1.4] [--enrol-jwt /path/monky-host.env-qa-02.jwt] \
|
# [--transport sdk|proxy|system] [--version 0.1.6] [--enrol-jwt /path/monky-host.env-qa-02.jwt] \
|
||||||
# [--laptop] [--source gitlab|gitea] [--docker-data-root /home/docker-data]
|
# [--laptop] [--source gitlab|gitea] [--docker-data-root /home/docker-data]
|
||||||
#
|
#
|
||||||
# --token / MONKY_DEPLOYD_TOKEN: the GitLab project is PRIVATE (its parent groups are private, so it
|
# --token / MONKY_DEPLOYD_TOKEN: the GitLab project is PRIVATE (its parent groups are private, so it
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
DEFAULT_VERSION="0.1.4"
|
DEFAULT_VERSION="0.1.6"
|
||||||
# Download source. PRIMARY is the GitLab project's generic package registry on scm.tikali.ai: inside
|
# Download source. PRIMARY is the GitLab project's generic package registry on scm.tikali.ai: inside
|
||||||
# the estate gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175), which has no HTTP
|
# the estate gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175), which has no HTTP
|
||||||
# ingress, so backend boxes cannot reach the Gitea mirror (cbs/iac#102); scm.tikali.ai they can. The
|
# ingress, so backend boxes cannot reach the Gitea mirror (cbs/iac#102); scm.tikali.ai they can. The
|
||||||
@@ -233,7 +233,7 @@ identity: $IDENTITY
|
|||||||
tenancy:
|
tenancy:
|
||||||
service: monky.tenancy.deploy
|
service: monky.tenancy.deploy
|
||||||
host: monky.tenancy.deploy
|
host: monky.tenancy.deploy
|
||||||
port: 8081
|
port: 443 # the service's INTERCEPT port (host.v1 forwards to 8081 inside the pod); plain HTTP inside the mesh
|
||||||
scheme: http
|
scheme: http
|
||||||
proxy_addr: 127.0.0.1:18443
|
proxy_addr: 127.0.0.1:18443
|
||||||
bao:
|
bao:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "monky-deployd"
|
name = "monky-deployd"
|
||||||
version = "0.1.4"
|
version = "0.1.6"
|
||||||
description = "Monky backend pull agent: checkin -> bundle -> lease -> OpenBao -> docker compose -> report, over the ziti mesh (MONKY-ADR-0028)"
|
description = "Monky backend pull agent: checkin -> bundle -> lease -> OpenBao -> docker compose -> report, over the ziti mesh (MONKY-ADR-0028)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+6
-3
@@ -190,9 +190,11 @@ class FakeBao:
|
|||||||
return h._send(400, {"errors": ["error validating token: expired"]})
|
return h._send(400, {"errors": ["error validating token: expired"]})
|
||||||
if body.get("role") != self.role:
|
if body.get("role") != self.role:
|
||||||
return h._send(400, {"errors": [f"role {body.get('role')!r} could not be found"]})
|
return h._send(400, {"errors": [f"role {body.get('role')!r} could not be found"]})
|
||||||
meta = self.grants.pop(body.get("jwt", ""), None) # single-use grant
|
# the jwt mount keeps no jti/replay state: a known grant logs in until it expires;
|
||||||
|
# a SUPERSEDED grant is refused by tenancy (FakeTenancy.superseded_jtis), not here
|
||||||
|
meta = self.grants.get(body.get("jwt", ""))
|
||||||
if meta is None:
|
if meta is None:
|
||||||
return h._send(400, {"errors": ["error validating token: unknown or already used grant"]})
|
return h._send(400, {"errors": ["error validating token: unknown grant"]})
|
||||||
m = json.loads(meta)
|
m = json.loads(meta)
|
||||||
tok = self.mint(m["env_id"])
|
tok = self.mint(m["env_id"])
|
||||||
self.tokens[tok]["grant_jti"] = m["jti"]
|
self.tokens[tok]["grant_jti"] = m["jti"]
|
||||||
@@ -354,7 +356,8 @@ class FakeTenancy:
|
|||||||
"ttl_s": 3600,
|
"ttl_s": 3600,
|
||||||
"mount": self.bao.mount,
|
"mount": self.bao.mount,
|
||||||
"role": self.bao.role,
|
"role": self.bao.role,
|
||||||
"vault": {"addr": "https://bao.cbs.tikali.net:8200", "mount": self.kv_mount},
|
# tenancy AgentLeaseOut: `addr` is top-level; KV mount/prefix come from checkin
|
||||||
|
"addr": "https://bao.cbs.tikali.net:8200",
|
||||||
},
|
},
|
||||||
headers={"Cache-Control": "no-store"},
|
headers={"Cache-Control": "no-store"},
|
||||||
)
|
)
|
||||||
|
|||||||
+28
-5
@@ -107,7 +107,7 @@ def test_rollback_refused_unless_allowed(bootstrapped, tenancy, fake_docker):
|
|||||||
tenancy.set_files(first)
|
tenancy.set_files(first)
|
||||||
assert tick(cfg) == EX_FAIL
|
assert tick(cfg) == EX_FAIL
|
||||||
assert tenancy.reports[-1]["result"] == "failed" and "ROLLBACK_REFUSED" in tenancy.reports[-1]["detail"]
|
assert tenancy.reports[-1]["result"] == "failed" and "ROLLBACK_REFUSED" in tenancy.reports[-1]["detail"]
|
||||||
assert len(tenancy.leases) == 2 # a refused bundle never leases
|
assert len(tenancy.leases) == 1 # a refused bundle never leases; the 2nd apply reused the lease token
|
||||||
tenancy.set_files(make_files(meta={"agent": {"allow_rollback": True}}))
|
tenancy.set_files(make_files(meta={"agent": {"allow_rollback": True}}))
|
||||||
assert tick(cfg) == EX_OK
|
assert tick(cfg) == EX_OK
|
||||||
|
|
||||||
@@ -246,10 +246,9 @@ def test_legacy_approle_lease_is_refused_loudly(bootstrapped, tenancy, fake_dock
|
|||||||
assert "compose up" not in fake_docker.subcommands()
|
assert "compose up" not in fake_docker.subcommands()
|
||||||
|
|
||||||
|
|
||||||
def test_lease_rate_limited_is_temporary(bootstrapped, tenancy, fake_docker):
|
# (test_lease_rate_limited_is_temporary was retired in 0.1.6: a rate-limited lease is only a
|
||||||
cfg = bootstrapped
|
# temporary failure when NO working token exists; with one, the agent applies and defers the swap —
|
||||||
tenancy.lease_limit = 0
|
# see test_rate_limited_lease_does_not_block_an_apply_when_a_token_exists.)
|
||||||
assert tick(cfg) == EX_TEMPFAIL
|
|
||||||
|
|
||||||
|
|
||||||
def test_unhealthy_after_up_reports_failed_with_compose_logs(bootstrapped, tenancy, fake_docker):
|
def test_unhealthy_after_up_reports_failed_with_compose_logs(bootstrapped, tenancy, fake_docker):
|
||||||
@@ -320,3 +319,27 @@ def test_write_private_mode(tmp_path):
|
|||||||
p = tmp_path / "d" / "f"
|
p = tmp_path / "d" / "f"
|
||||||
statemod.write_private(p, b"x")
|
statemod.write_private(p, b"x")
|
||||||
assert oct(p.stat().st_mode & 0o777) == "0o600" and not any(n.startswith(".f.") for n in os.listdir(p.parent))
|
assert oct(p.stat().st_mode & 0o777) == "0o600" and not any(n.startswith(".f.") for n in os.listdir(p.parent))
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_apply_reuses_the_lease_token(bootstrapped, tenancy, bao, fake_docker):
|
||||||
|
"""A valid lease-derived token is reused: a new bundle does NOT lease again (5/h budget —
|
||||||
|
env-qa-02 pilot: a retried deploy re-leased every 60 s and hit LEASE_RATE_LIMITED forever)."""
|
||||||
|
cfg = bootstrapped
|
||||||
|
assert tick(cfg) == EX_OK
|
||||||
|
assert len(tenancy.leases) == 1
|
||||||
|
tenancy.set_files(make_files(manifest=make_manifest(versions={"gemini_api_key": 1})))
|
||||||
|
assert tick(cfg) == EX_OK
|
||||||
|
assert len(tenancy.leases) == 1 # reused
|
||||||
|
st = statemod.load(cfg.state_path, ENV)
|
||||||
|
assert st.applied_sha == tenancy.desired_sha and st.token.source == "lease"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rate_limited_lease_does_not_block_an_apply_when_a_token_exists(bootstrapped, tenancy, bao, fake_docker):
|
||||||
|
"""tenancy 429 on lease while the bootstrap token still works → apply with it, swap deferred."""
|
||||||
|
cfg = bootstrapped
|
||||||
|
tenancy.lease_limit = 0
|
||||||
|
assert tick(cfg) == EX_OK
|
||||||
|
assert tenancy.leases == []
|
||||||
|
st = statemod.load(cfg.state_path, ENV)
|
||||||
|
assert st.applied_sha == tenancy.desired_sha and st.token.source == "bootstrap"
|
||||||
|
assert tenancy.reports[-1]["result"] == "applied"
|
||||||
|
|||||||
@@ -79,3 +79,16 @@ def test_disk_need_bytes_spellings():
|
|||||||
assert b.parse(tar_bytes(make_files(meta={"agent": {"disk_need_bytes": 5}}))).disk_need_bytes == 5
|
assert b.parse(tar_bytes(make_files(meta={"agent": {"disk_need_bytes": 5}}))).disk_need_bytes == 5
|
||||||
assert b.parse(tar_bytes(make_files(meta={"disk": {"need_bytes": 7}}))).disk_need_bytes == 7
|
assert b.parse(tar_bytes(make_files(meta={"disk": {"need_bytes": 7}}))).disk_need_bytes == 7
|
||||||
assert b.parse(tar_bytes(make_files())).disk_need_bytes == 0
|
assert b.parse(tar_bytes(make_files())).disk_need_bytes == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_placeholders_in_comment_lines_are_not_references():
|
||||||
|
"""The renderer's .env.template header says '... substitutes every ${VAR} ...' — that must not
|
||||||
|
become an unresolved 'VAR' (env-qa-02 pilot: ENV_INCOMPLETE: unresolved: VAR)."""
|
||||||
|
from monky_deployd.bundle import defaulted_vars, referenced_vars
|
||||||
|
|
||||||
|
text = (
|
||||||
|
"# The on-box agent substitutes every ${VAR} from OpenBao per secrets.manifest.json.\n"
|
||||||
|
" # ${ALSO_COMMENT}\nGEMINI_API_KEY=${GEMINI_API_KEY}\nPG=${PGPASSWORD:-x}\n"
|
||||||
|
)
|
||||||
|
assert referenced_vars(text) == {"GEMINI_API_KEY", "PGPASSWORD"}
|
||||||
|
assert defaulted_vars(text) == {"PGPASSWORD"}
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ def test_proxy_transport_refuses_unmapped_hosts():
|
|||||||
|
|
||||||
cfg = from_dict({"env_id": "env-dev-06", "site": "cbs", "transport": "proxy"})
|
cfg = from_dict({"env_id": "env-dev-06", "site": "cbs", "transport": "proxy"})
|
||||||
t = build(cfg)
|
t = build(cfg)
|
||||||
assert t.mapping[("monky.tenancy.deploy", 8081)] == ("127.0.0.1", 18443)
|
assert t.mapping[("monky.tenancy.deploy", 443)] == ("127.0.0.1", 18443)
|
||||||
assert t.mapping[("bao.cbs.tikali.net", 8200)] == ("127.0.0.1", 18200)
|
assert t.mapping[("bao.cbs.tikali.net", 8200)] == ("127.0.0.1", 18200)
|
||||||
try:
|
try:
|
||||||
t.connect("example.com", 443, 1)
|
t.connect("example.com", 443, 1)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ transport: sdk
|
|||||||
identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json
|
identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json
|
||||||
tenancy:
|
tenancy:
|
||||||
service: monky.tenancy.deploy
|
service: monky.tenancy.deploy
|
||||||
base_url: http://monky.tenancy.deploy:8081
|
base_url: http://monky.tenancy.deploy:443
|
||||||
bao:
|
bao:
|
||||||
service: openbao
|
service: openbao
|
||||||
addr: https://bao.cbs.tikali.net:8200 # intercept, not public DNS
|
addr: https://bao.cbs.tikali.net:8200 # intercept, not public DNS
|
||||||
@@ -27,7 +27,7 @@ volumes_on_absent: keep
|
|||||||
def test_yaml_subset_parses_nested_maps_and_types():
|
def test_yaml_subset_parses_nested_maps_and_types():
|
||||||
d = c.parse_yaml_subset(KIT)
|
d = c.parse_yaml_subset(KIT)
|
||||||
assert d["env_id"] == "env-qa-02"
|
assert d["env_id"] == "env-qa-02"
|
||||||
assert d["tenancy"]["base_url"] == "http://monky.tenancy.deploy:8081"
|
assert d["tenancy"]["base_url"] == "http://monky.tenancy.deploy:443"
|
||||||
assert d["bao"]["addr"] == "https://bao.cbs.tikali.net:8200"
|
assert d["bao"]["addr"] == "https://bao.cbs.tikali.net:8200"
|
||||||
assert d["interval_s"] == 60 and d["laptop_mode"] is False
|
assert d["interval_s"] == 60 and d["laptop_mode"] is False
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def test_yaml_subset_refuses_flow_style_and_tabs():
|
|||||||
|
|
||||||
def test_config_defaults_and_derivations():
|
def test_config_defaults_and_derivations():
|
||||||
cfg = c.from_dict(c.parse_yaml_subset(KIT))
|
cfg = c.from_dict(c.parse_yaml_subset(KIT))
|
||||||
assert cfg.tenancy.host == "monky.tenancy.deploy" and cfg.tenancy.port == 8081 and cfg.tenancy.scheme == "http"
|
assert cfg.tenancy.host == "monky.tenancy.deploy" and cfg.tenancy.port == 443 and cfg.tenancy.scheme == "http"
|
||||||
assert cfg.bao_url == ("https", "bao.cbs.tikali.net", 8200)
|
assert cfg.bao_url == ("https", "bao.cbs.tikali.net", 8200)
|
||||||
assert cfg.deploy_dir == "/var/lib/monky-deployd/env-qa-02"
|
assert cfg.deploy_dir == "/var/lib/monky-deployd/env-qa-02"
|
||||||
assert cfg.compose_project == "monky-env-qa-02"
|
assert cfg.compose_project == "monky-env-qa-02"
|
||||||
|
|||||||
Reference in New Issue
Block a user