6 Commits

Author SHA1 Message Date
monky-deployd-merger 7343970e2e Merge branch 'fix/version-0.1.10' into 'main'
chore: __version__ 0.1.10

See merge request tikali/applications/monky/monky-deployd!16
2026-09-13 00:26:21 +00:00
mdella 96c5dbbeb5 chore: __version__ 0.1.10 (the tag guard caught pyproject-only bump; tag pipeline 7977)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASnneBmT7rfaJLE8NGNw7S
2026-09-13 00:25:11 +00:00
monky-deployd-merger fead1f999b Merge branch 'fix/sites-fmt-roam-and-identity-acl' into 'main'
fix: sites fmt|cbs|pdx|roam + identity ACL watcher — 0.1.10 (deployd#3)

Closes #3

See merge request tikali/applications/monky/monky-deployd!15
2026-09-13 00:21:30 +00:00
mdella 037782e1ff fix: accept sites fmt|cbs|pdx|roam (tenancy 0.7.x) and keep the identity read grant alive across tunneller rewrites — 0.1.10
deployd#3 (DD-0620): every kit for a backend registered since 2026-09-08 died at `--site`.
env-dev-08 (2026-09-11..13): two days of "identity is not readable" ticks — ziti-edge-tunnel
re-creates the file with mode 0600, the ACL mask goes to ---, group membership stops helping.
identity-acl.sh + monky-deployd-identity-acl.path re-apply the grant on every directory change.

Doc-Drift: DD-0620 fixed
Closes #3

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASnneBmT7rfaJLE8NGNw7S
2026-09-13 00:17:35 +00:00
mdella fe7b0c6922 Merge branch 'fix/upgrade-keeps-the-timer' into 'main'
fix(packaging): an upgrade must not stop and disable the agent — 0.1.9

See merge request tikali/applications/monky/monky-deployd!14
2026-09-08 21:10:53 -07:00
mdella 4ff2e29fbc fix(packaging): an upgrade must not stop and disable the agent — 0.1.9
dpkg calls the OLD package's prerm on an UPGRADE as well as on a removal (rpm passes a
remaining-instance count), and preremove.sh ran `systemctl disable --now monky-deployd.timer`
unconditionally. Upgrading env-dev-01 and env-dev-08 to 0.1.8 today stopped and disabled both
agents.

The failure is silent, which is the dangerous part: the box stays reachable, the containers keep
running, and nothing reports that check-ins have ceased — the backend just stops converging. A
fleet upgrade would have taken every agent offline at once and looked like a success.

preremove.sh now returns early for every upgrade shape (upgrade, failed-upgrade, deconfigure,
rpm's 1) and only disables on a real removal. postinstall.sh try-restarts the long-lived proxy
unit so it picks up the new code; the timer needs nothing, since each tick is a fresh process.

Tests drive the script with a fake systemctl on PATH and assert an upgrade touches no units.
OPERATIONS.md warns that a box coming FROM 0.1.8 or earlier still needs its timer re-enabled by
hand, because the old prerm has already run by then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
2026-09-09 04:07:30 +00:00
15 changed files with 165 additions and 11 deletions
+26
View File
@@ -1,6 +1,32 @@
<!-- xlate:verbatim-fences -->
# Changelog
## 0.1.10 — sites `fmt|pdx|roam`, and a read grant that outlives the tunneller (2026-09-13)
- **Install kits for every backend registered since 2026-09-08 died at `--site`** (deployd#3,
DD-0620): tenancy 0.7.x issues `fmt | pdx | roam`, deployd still enforced `cbs | pdx`. `SITES`,
`install.sh` and `config.example.yaml` now accept `fmt | cbs | pdx | roam` (`cbs` = the deprecated
alias of `fmt` on rows registered before the rename) and a test pins the set to tenancy's.
- **The agent's read on the ziti identity now survives a tunneller rewrite for real.** 0.1.8 made
`monky-deployd` a member of the `ziti` group; that is not enough: `ziti-edge-tunnel` re-creates the
file with mode `0600`, which sets the ACL mask to `---` and defeats the group read and the named-user
entry alike (env-dev-08, 2026-09-11 → 09-13: two days of *"identity is not readable"* ticks under a
healthy tunnel; the backend went `offline`). New `identity-acl.sh` re-applies the entries **and the
mask**; `monky-deployd-identity-acl.path` re-runs it whenever the identity directory changes;
`install.sh` and the package postinstall arm it.
## 0.1.9 — an upgrade no longer stops the agent (2026-09-09)
- **`dpkg -i` over a running agent disabled it.** dpkg calls the OLD package's `prerm` on an
**upgrade** as well as on a removal (rpm passes a remaining-instance count), and `preremove.sh`
ran `systemctl disable --now monky-deployd.timer` unconditionally. Upgrading env-dev-01 and
env-dev-08 from 0.1.6/0.1.7 to 0.1.8 stopped and **disabled** both agents. It is silent: the box
stays up, the containers keep running, and nothing reports that check-ins have ceased — the
backend simply stops converging. `preremove.sh` now returns early for every upgrade shape
(`upgrade`, `failed-upgrade`, `deconfigure`, rpm's `1`), and `postinstall.sh` `try-restart`s the
long-lived proxy unit so it picks up the new code. A fleet upgrade would have taken every agent
offline at once.
## 0.1.8 — onboarding: keep the identity readable, refuse a full disk (2026-09-09)
Three faults from one onboarding (env-dev-08, agent-managed, 2026-09-09), each of which sent the
+1 -1
View File
@@ -3,7 +3,7 @@
# simple lists, comments. Keys not listed here are a config error.
env_id: env-qa-02 # env-<tier>-<nn> (or a grandfathered legacy id); MUST match the token's env
site: cbs # cbs | pdx (lowercase DC code)
site: cbs # fmt | pdx | roam as tenancy issues it (cbs = deprecated alias of fmt, still on rows registered before 2026-09-08)
transport: sdk # sdk (OpenZiti Python SDK, default) | proxy (monky-deployd-proxy.service) | system (tunneler `run` mode / plain DNS)
identity: /opt/openziti/etc/identities/monky-host.env-qa-02.json # the box's host identity (read via ACL)
+6 -1
View File
@@ -9,6 +9,7 @@
| `monky-deployd.service` | `Type=oneshot`, `monky-deployd run --once` as user `monky-deployd` (+ `docker` group); `SuccessExitStatus=75`; hardened (`NoNewPrivileges`, `ProtectSystem=strict`, `ReadWritePaths=/var/lib/monky-deployd /etc/monky-deployd /run/docker.sock`, `UMask=0077`, no capabilities) |
| `monky-deployd-proxy.service` | only with `transport: proxy`: `ziti tunnel proxy -i <identity> monky.tenancy.deploy:18443 openbao:18200` as user `ziti`; `EnvironmentFile=/etc/monky-deployd/proxy.env` |
| `ziti-edge-tunnel.service` | the host identity's tunneler in **`run-host`** mode (drop-in `run-host.conf` written by `install.sh`) |
| `monky-deployd-identity-acl.path` | re-runs `identity-acl.sh` whenever `/opt/openziti/etc/identities` changes, so the agent keeps its read after the tunneller rewrites the identity (0.1.10) |
```sh
systemctl status monky-deployd.timer monky-deployd.service
@@ -30,6 +31,10 @@ the `ziti` **group**, plus an explicit ACL `u:monky-deployd:r` and a default ACL
> first refresh and then fails every tick (env-dev-08, 2026-09-09). On a host where the tunneller was
> installed independently of the kit, check `id monky-deployd` for `ziti` before anything else.
> **Upgrading the agent.** `dpkg -i` keeps the timer enabled from 0.1.9 on. On a box upgraded from
> 0.1.8 or earlier the old package's `prerm` already ran and **disabled** it — check
> `systemctl is-enabled monky-deployd.timer` after the upgrade and `systemctl enable --now` it if needed.
## Reading the journal
| line | meaning |
@@ -40,7 +45,7 @@ the `ziti` **group**, plus an explicit ACL `u:monky-deployd:r` and a default ACL
| `refused: DISK_INSUFFICIENT: image storage (…) has N MiB free, …` | free space: grow the disk or prune. The paths in the message are every filesystem a pull can fill — docker's data-root AND containerd's root, which docker 29 fills with image layers and which is often a different disk (env-dev-08). A bundle that declares no size must still clear `disk.headroom_bytes` |
| `refused: PRIVILEGED_REFUSED` / `ROLLBACK_REFUSED` | the bundle needs `allow_privileged` / `allow_rollback` in its `agent` profile |
| `temporary network failure` (exit 75) | mesh/tenancy unreachable — check `ziti-edge-tunnel`, the identity's terminators, `monky.tenancy.deploy` health |
| `transport sdk: identity … is not readable by this user` | the agent lost its read on the identity file (usually a tunneller rewrite dropping the ACL) — `usermod -a -G ziti monky-deployd` |
| `transport sdk: identity … is not readable by this user` | the agent lost its read on the identity file `ziti-edge-tunnel` re-created it with mode `0600`, which sets the ACL mask to `---` (group membership does not help then). Run `/usr/share/monky-deployd/identity-acl.sh` and check `systemctl is-active monky-deployd-identity-acl.path` (0.1.10 re-applies it on every directory change) |
| `AGENT_UNAUTHENTICATED: bearer refused` (exit 1) | the grant was superseded (kit re-revealed / retire) or the token revoked → re-run the install kit |
| `AGENT_ENV_MISMATCH` (exit 78) | the token belongs to another env than `config.yaml` — fix the config or re-issue the identity; the timer keeps firing but every tick exits 78 immediately (no storm) |
| `failed: docker compose pull failed (rc=1)` | registry/pull problem; compose output is in the report's tail and in the journal |
+1 -1
View File
@@ -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.8"
__version__ = "0.1.10"
+3 -1
View File
@@ -15,7 +15,9 @@ from pathlib import Path
DEFAULT_CONFIG_PATH = "/etc/monky-deployd/config.yaml"
TRANSPORTS = ("sdk", "proxy", "system")
ENV_ID_RE = re.compile(r"^env-(dev|qa|stage|prod)-[0-9]{2,3}$|^(dev-env-2|prod-cedar)$")
SITES = ("cbs", "pdx")
# tenancy 0.7.x issues `fmt | pdx | roam` (doc 24 §1.4a); `cbs` is the deprecated alias of `fmt`
# that every row registered before 2026-09-08 still carries. Keep the two sets in step (deployd#3).
SITES = ("fmt", "cbs", "pdx", "roam")
class ConfigError(Exception):
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# monky-deployd: (re)grant the agent read access to the host's ziti identity file(s).
# Idempotent; safe to run at any time. Invoked by install.sh, the package postinstall and the
# monky-deployd-identity-acl.path unit (whenever the identity directory changes).
#
# Why a re-runnable script and not a one-time ACL: ziti-edge-tunnel re-creates the identity on
# every controller config update with mode 0600. On a file with an ACL that sets the mask to ---,
# so the named-user entry AND the ziti-group read both become ineffective. Only re-applying the
# entries after each rewrite keeps the agent alive.
set -eu
DIR="${1:-/opt/openziti/etc/identities}"
[ -d "$DIR" ] || exit 0
command -v setfacl >/dev/null 2>&1 || exit 0
setfacl -m u:monky-deployd:rx,m::rx "$DIR" 2>/dev/null || true
setfacl -d -m u:monky-deployd:r,m::r "$DIR" 2>/dev/null || true
for f in "$DIR"/*.json; do
[ -f "$f" ] || continue
setfacl -m u:monky-deployd:r,m::r "$f" 2>/dev/null || true
done
exit 0
+9 -5
View File
@@ -4,7 +4,7 @@
# T=<read-only GitLab deploy token, scope read_package_registry> # 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.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 fmt --token "$T" --bootstrap-file bootstrap.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]
#
@@ -77,7 +77,7 @@ done
[ -n "$SITE" ] || die "--site is required"
[[ "$ENV_ID" =~ ^env-(dev|qa|stage|prod)-[0-9]{2,3}$|^(dev-env-2|prod-cedar)$ ]] || die "env id $ENV_ID is not env-<tier>-<nn>"
SITE="${SITE,,}"
[[ "$SITE" =~ ^(cbs|pdx)$ ]] || die "site must be cbs|pdx"
[[ "$SITE" =~ ^(fmt|cbs|pdx|roam)$ ]] || die "site must be fmt|cbs|pdx|roam (tenancy issues fmt|pdx|roam; cbs = the deprecated alias of fmt)"
[[ "$TRANSPORT" =~ ^(sdk|proxy|system)$ ]] || die "transport must be sdk|proxy|system"
[[ "$SOURCE" =~ ^(gitlab|gitea)$ ]] || die "source must be gitlab|gitea"
# never let the token leak through xtrace / the environment of children
@@ -219,10 +219,14 @@ install -d -m 0700 -o monky-deployd -g monky-deployd /var/lib/monky-deployd
# window before the agent's next login and hosts where the file has another group.
# A default ACL on the directory carries the grant onto a freshly created identity file.
if getent group ziti >/dev/null; then usermod -a -G ziti monky-deployd || true; fi
setfacl -m u:monky-deployd:r "$IDENTITY"
setfacl -m u:monky-deployd:rx "$IDENTITY_DIR"
setfacl -d -m u:monky-deployd:r "$IDENTITY_DIR" 2>/dev/null || true
setfacl -m u:monky-deployd:x /opt/openziti/etc 2>/dev/null || true
# The grant must OUTLIVE the tunneller: ziti-edge-tunnel re-creates the identity file on every
# controller config update with mode 0600, which sets the ACL mask to --- and defeats both the
# named-user entry and the ziti-group read (env-dev-08, 2026-09-11..13: two days of "identity is
# not readable" ticks while the tunnel was healthy). identity-acl.sh re-applies the entries and the
# mask; the .path unit re-runs it whenever the directory changes.
/usr/share/monky-deployd/identity-acl.sh "$IDENTITY_DIR"
systemctl enable --now monky-deployd-identity-acl.path 2>/dev/null || true
if [ -n "$BAO_CA" ]; then
install -m 0644 "$BAO_CA" "$ETC/openbao-ca.pem"
fi
+8
View File
@@ -33,6 +33,14 @@ contents:
dst: /usr/lib/systemd/system/monky-deployd.timer
- src: ./packaging/systemd/monky-deployd-proxy.service
dst: /usr/lib/systemd/system/monky-deployd-proxy.service
- src: ./packaging/systemd/monky-deployd-identity-acl.path
dst: /usr/lib/systemd/system/monky-deployd-identity-acl.path
- src: ./packaging/systemd/monky-deployd-identity-acl.service
dst: /usr/lib/systemd/system/monky-deployd-identity-acl.service
- src: ./packaging/bin/identity-acl.sh
dst: /usr/share/monky-deployd/identity-acl.sh
file_info:
mode: 0755
- src: ./packaging/monky-deployd.sysusers
dst: /usr/lib/sysusers.d/monky-deployd.conf
- src: ./packaging/monky-deployd.tmpfiles
+9 -1
View File
@@ -18,10 +18,18 @@ if getent group docker >/dev/null; then usermod -a -G docker monky-deployd || tr
# POSIX ACL granting the agent read — group membership is the grant that survives it.
# (env-dev-08, 2026-09-09: the agent went from applied to "no intercept" 6 minutes after a refresh.)
if getent group ziti >/dev/null; then usermod -a -G ziti monky-deployd || true; fi
# ...and group membership does NOT survive it either once the rewrite lands with mode 0600 (the ACL
# mask goes to ---). Re-apply the grant now and on every directory change (0.1.10, deployd#3 beat).
[ -x /usr/share/monky-deployd/identity-acl.sh ] && /usr/share/monky-deployd/identity-acl.sh /opt/openziti/etc/identities || true
# the venv is relocatable only to the path it was built at; refuse a broken interpreter early
/opt/monky-deployd/venv/bin/python -c 'import monky_deployd' || { echo "monky-deployd: venv unusable (python3 mismatch?)" >&2; exit 1; }
if [ -d /run/systemd/system ]; then
systemctl daemon-reload || true
# do NOT enable the timer here: install.sh / the ansible role do it after the config exists
# do NOT enable the timer here: install.sh / the ansible role do it after the config exists.
# An UPGRADE is different — the timer is already enabled and must keep running, so restart the
# long-lived proxy unit onto the new code. `try-restart` is a no-op when it is not running.
systemctl try-restart monky-deployd-proxy.service 2>/dev/null || true
# an UPGRADE of a box that already has its config: arm the identity-ACL watcher
if [ -s /etc/monky-deployd/config.yaml ]; then systemctl enable --now monky-deployd-identity-acl.path 2>/dev/null || true; fi
fi
exit 0
+9
View File
@@ -1,5 +1,14 @@
#!/bin/sh
set -e
# dpkg calls the OLD package's prerm on an UPGRADE as well as on a removal, and rpm calls it with
# an install count. Disabling the timer unconditionally therefore stopped the agent on every
# upgrade and left it disabled — silently, because the box stays reachable and nothing else
# notices that check-ins have ceased (env-dev-01 and env-dev-08, 2026-09-09).
# dpkg: "$1" is `remove`, `upgrade <new-version>`, `deconfigure …` or `failed-upgrade`
# rpm : "$1" is the number of instances that will remain — 1 on upgrade, 0 on uninstall
case "${1:-}" in
upgrade | failed-upgrade | deconfigure | 1) exit 0 ;;
esac
if [ -d /run/systemd/system ]; then
systemctl disable --now monky-deployd.timer 2>/dev/null || true
systemctl disable --now monky-deployd-proxy.service 2>/dev/null || true
@@ -0,0 +1,11 @@
[Unit]
Description=Re-grant monky-deployd read access when the ziti identity directory changes
Documentation=file:/usr/share/doc/monky-deployd/OPERATIONS.md
[Path]
PathChanged=/opt/openziti/etc/identities
PathModified=/opt/openziti/etc/identities
Unit=monky-deployd-identity-acl.service
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,6 @@
[Unit]
Description=Re-grant monky-deployd read access to the ziti identity file(s)
[Service]
Type=oneshot
ExecStart=/usr/share/monky-deployd/identity-acl.sh /opt/openziti/etc/identities
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "monky-deployd"
version = "0.1.8"
version = "0.1.10"
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"
+9
View File
@@ -74,6 +74,15 @@ def test_config_rejects_approle_and_unknown_keys():
c.from_dict({"env_id": "env-dev-06", "site": "sfo"})
def test_config_sites_match_tenancy():
"""tenancy 0.7.x registers `fmt | pdx | roam`; `cbs` stays as the deprecated alias every row
registered before 2026-09-08 carries (deployd#3: kits died with 'site must be cbs|pdx')."""
assert set(c.SITES) == {"fmt", "cbs", "pdx", "roam"}
for site in ("fmt", "roam", "pdx", "cbs"):
assert c.from_dict({"env_id": "env-dev-06", "site": site}).site == site
assert c.from_dict({"env_id": "env-dev-06", "site": "FMT"}).site == "fmt"
def test_sdk_identity_defaults_to_host_identity():
cfg = c.from_dict({"env_id": "env-dev-07", "site": "cbs"})
assert cfg.identity == "/opt/openziti/etc/identities/monky-host.env-dev-07.json"
+46
View File
@@ -0,0 +1,46 @@
"""The maintainer scripts must not stop the agent on an upgrade.
dpkg calls the OLD package's prerm on an upgrade as well as on a removal, and rpm calls it with
an install count. `systemctl disable --now` there stopped the agent on every upgrade and left it
disabled silently, because the box stays reachable and nothing else notices that check-ins have
ceased (env-dev-01 and env-dev-08, 2026-09-09).
"""
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
import pytest
PREREMOVE = Path(__file__).resolve().parents[1] / "packaging" / "scripts" / "preremove.sh"
def _run(arg: str, tmp_path: Path) -> list[str]:
"""Run preremove with a fake `systemctl` on PATH and report the calls it made."""
calls = tmp_path / "systemctl.log"
fake = tmp_path / "bin"
fake.mkdir(exist_ok=True)
(fake / "systemctl").write_text(f'#!/bin/sh\necho "$@" >> {calls}\nexit 0\n')
(fake / "systemctl").chmod(0o755)
env = {"PATH": f"{fake}:/usr/bin:/bin"}
r = subprocess.run(["sh", str(PREREMOVE), arg], env=env, capture_output=True, text=True)
assert r.returncode == 0, r.stderr
return calls.read_text().splitlines() if calls.exists() else []
@pytest.mark.skipif(not Path("/run/systemd/system").is_dir(), reason="needs a systemd host to reach the disable branch")
@pytest.mark.parametrize("arg", ["remove", "0"])
def test_a_real_removal_disables_the_units(arg, tmp_path):
assert any("disable" in c for c in _run(arg, tmp_path))
@pytest.mark.parametrize("arg", ["upgrade", "failed-upgrade", "deconfigure", "1"])
def test_an_upgrade_leaves_the_units_alone(arg, tmp_path):
assert _run(arg, tmp_path) == []
def test_the_script_is_shell_clean():
assert shutil.which("sh")
subprocess.run(["sh", "-n", str(PREREMOVE)], check=True)