mirror of
https://scm.tikali.ai/tikali/applications/monky/monky-deployd.git
synced 2026-09-18 05:36:15 +00:00
feat: pull private images without a hand docker login; surface the pull error
Design merged first: monky-design-docs !225 (doc 24 §4a). Pairs with monky-tenancy!40, which copies the estate-wide read-only Harbor robot into each environment's own prefix and marks the manifest entry `use: registry-auth`. - That entry is not an env var (it would otherwise land in .env and therefore in every container's environment). The agent parses it — JSON, or `username:password` with the new `registry_host` — and writes `<state_dir>/docker/config.json` 0600 in a directory it owns, with an explicit DOCKER_CONFIG pointing the docker CLI at it. The unit runs as monky-deployd, so a `docker login` by a human or by root is invisible to the agent: that is what made env-dev-01 look like it had no credential at all after the operator had just logged in. - `compose pull` failures now carry the registry's own message ("no basic auth credentials", "manifest unknown", DNS) into the journal and the report instead of `rc=1`. - Tests: both credential shapes, the refusals, 0600/0700 modes, idempotent rewrite, and that the runner never silently falls back to a human's $HOME. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
This commit is contained in:
+32
-5
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
|
||||
from monky_deployd import __version__
|
||||
from monky_deployd import bundle as bundlemod
|
||||
from monky_deployd import registry as registrymod
|
||||
from monky_deployd import state as statemod
|
||||
from monky_deployd.bao import BaoClient, BaoError, BaoToken, ManifestPathError, kv_data_path
|
||||
from monky_deployd.bundle import Bundle, BundleError
|
||||
@@ -86,7 +87,7 @@ class Agent:
|
||||
def __init__(self, cfg: Config, *, prune: bool = False, docker: Docker | None = None):
|
||||
self.cfg = cfg
|
||||
self.prune = prune
|
||||
self.docker = docker or Docker(cfg.docker_bin)
|
||||
self.docker = docker or Docker(cfg.docker_bin, docker_config=str(cfg.docker_config_dir))
|
||||
self.transport = build(cfg)
|
||||
self.tenancy_http = HttpClient(
|
||||
self.transport, cfg.tenancy.scheme, cfg.tenancy.host, cfg.tenancy.port, timeout=cfg.tenancy.timeout_s
|
||||
@@ -274,13 +275,29 @@ class Agent:
|
||||
if hdr_sha and hdr_sha != b.sha:
|
||||
log.warning("bundle header sha %s disagrees with content %s", _short(hdr_sha), _short(b.sha))
|
||||
self._refusal_checks(b)
|
||||
# secrets: lease -> login -> reads (values never logged; names only)
|
||||
# secrets: lease -> login -> reads (values never logged; names only). An entry marked
|
||||
# `use: registry-auth` is NOT an env var — it is the pull credential, written to the
|
||||
# agent's own Docker config instead of `.env` (doc 24 §4a).
|
||||
entries = b.manifest.get("entries", [])
|
||||
registry_entries = [e for e in entries if e.get("use") == registrymod.USE]
|
||||
env_entries = [e for e in entries if e.get("use") != registrymod.USE]
|
||||
if entries:
|
||||
token = self._token_for_apply()
|
||||
for e in entries:
|
||||
for e in env_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)))
|
||||
for e in registry_entries:
|
||||
raw = self.bao.kv_read(token, e["path"], cfg.env_id, e.get("version"))
|
||||
try:
|
||||
auth = registrymod.parse(raw, default_registry=cfg.registry_host)
|
||||
registrymod.write_docker_config(cfg.docker_config_dir, auth)
|
||||
except registrymod.RegistryAuthError as exc:
|
||||
log.warning("registry credential unusable (%s) — private images will not pull", exc)
|
||||
log.info(
|
||||
"read %d secret(s): %s%s",
|
||||
len(entries),
|
||||
", ".join(sorted(self._values)),
|
||||
" (+ registry credential)" if registry_entries else "",
|
||||
)
|
||||
env_text = bundlemod.render_env(b.env_template, self._values)
|
||||
leftover = bundlemod.referenced_vars(env_text)
|
||||
if leftover:
|
||||
@@ -288,7 +305,17 @@ class Agent:
|
||||
release = self._promote(self._stage(b, env_text))
|
||||
compose = self._compose()
|
||||
log.info("compose pull")
|
||||
compose.pull()
|
||||
try:
|
||||
compose.pull()
|
||||
except ComposeError as exc:
|
||||
# the registry's own message is the diagnosis ("no basic auth credentials", "manifest
|
||||
# unknown", a DNS failure). Swallowing it behind `rc=1` cost an SSH hunt on env-dev-01.
|
||||
detail = " | ".join(line.strip() for line in (exc.output or "").splitlines() if line.strip())[-600:]
|
||||
self.state.last_result = "failed"
|
||||
self.state.last_error = f"compose pull failed: {detail or exc}"
|
||||
log.error("compose pull failed (rc=%s): %s", exc.rc, detail or "(no output)")
|
||||
self._report("failed", b.sha, detail=f"compose pull: {detail or exc}"[:900])
|
||||
return EX_FAIL
|
||||
log.info("compose up -d --remove-orphans")
|
||||
compose.up()
|
||||
ok, containers = compose.wait_healthy(cfg.healthy_timeout_s)
|
||||
|
||||
Reference in New Issue
Block a user