diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a095d..152b873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # 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 diff --git a/README.md b/README.md index ffc0756..0552085 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ you the enrolment JWT, a one-time **bootstrap deploy grant** and the read-only * ```sh T= # 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.5/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 -# [--transport sdk|proxy|system] [--version 0.1.5] [--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 diff --git a/ansible/roles/monky_deployd/README.md b/ansible/roles/monky_deployd/README.md index 6615ff5..c69606d 100644 --- a/ansible/roles/monky_deployd/README.md +++ b/ansible/roles/monky_deployd/README.md @@ -16,7 +16,7 @@ template → optional openbao-ca PEM → optional `monky-deployd-proxy.service` | var | note | |---|---| -| `monky_deployd_version` | pinned release, e.g. `0.1.5` | +| `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_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 | diff --git a/ansible/roles/monky_deployd/defaults/main.yml b/ansible/roles/monky_deployd/defaults/main.yml index 4a879b1..5bd2f33 100644 --- a/ansible/roles/monky_deployd/defaults/main.yml +++ b/ansible/roles/monky_deployd/defaults/main.yml @@ -1,7 +1,7 @@ --- # 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. -monky_deployd_version: "0.1.5" +monky_deployd_version: "0.1.6" 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 # gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175, no HTTP ingress), so backend diff --git a/config.example.yaml b/config.example.yaml index 3aac077..e2c714d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,4 +1,4 @@ -# /etc/monky-deployd/config.yaml — monky-deployd v0.1.5 (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, # simple lists, comments. Keys not listed here are a config error. diff --git a/monky_deployd/__init__.py b/monky_deployd/__init__.py index 38981a2..454ff37 100644 --- a/monky_deployd/__init__.py +++ b/monky_deployd/__init__.py @@ -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`, reports. Stdlib only; the optional `openziti` SDK is the `sdk` transport.""" -__version__ = "0.1.5" +__version__ = "0.1.6" diff --git a/monky_deployd/agent.py b/monky_deployd/agent.py index 6eb3258..59f1817 100644 --- a/monky_deployd/agent.py +++ b/monky_deployd/agent.py @@ -277,7 +277,7 @@ class Agent: # secrets: lease -> login -> reads (values never logged; names only) entries = b.manifest.get("entries", []) if entries: - token = self._lease_login("apply") + token = self._token_for_apply() for e in entries: 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))) @@ -392,6 +392,29 @@ class Agent: if old and old != token: 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: assert self.tenancy is not None lease = self.tenancy.lease(reason) diff --git a/packaging/install.sh b/packaging/install.sh index 565b064..885d993 100755 --- a/packaging/install.sh +++ b/packaging/install.sh @@ -3,9 +3,9 @@ # # T= # from the install kit / OpenBao # curl -sSf -H "DEPLOY-TOKEN: $T" \ -# https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd/0.1.5/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 \ -# [--transport sdk|proxy|system] [--version 0.1.5] [--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] # # --token / MONKY_DEPLOYD_TOKEN: the GitLab project is PRIVATE (its parent groups are private, so it @@ -29,7 +29,7 @@ set -euo pipefail umask 077 -DEFAULT_VERSION="0.1.5" +DEFAULT_VERSION="0.1.6" # 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 # ingress, so backend boxes cannot reach the Gitea mirror (cbs/iac#102); scm.tikali.ai they can. The diff --git a/pyproject.toml b/pyproject.toml index f75f515..9886552 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "monky-deployd" -version = "0.1.5" +version = "0.1.6" description = "Monky backend pull agent: checkin -> bundle -> lease -> OpenBao -> docker compose -> report, over the ziti mesh (MONKY-ADR-0028)" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/test_agent.py b/tests/test_agent.py index 6ed4b4f..e35d889 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -107,7 +107,7 @@ def test_rollback_refused_unless_allowed(bootstrapped, tenancy, fake_docker): tenancy.set_files(first) assert tick(cfg) == EX_FAIL 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}})) 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() -def test_lease_rate_limited_is_temporary(bootstrapped, tenancy, fake_docker): - cfg = bootstrapped - tenancy.lease_limit = 0 - assert tick(cfg) == EX_TEMPFAIL +# (test_lease_rate_limited_is_temporary was retired in 0.1.6: a rate-limited lease is only a +# temporary failure when NO working token exists; with one, the agent applies and defers the swap — +# see test_rate_limited_lease_does_not_block_an_apply_when_a_token_exists.) 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" 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)) + + +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"