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:
@@ -0,0 +1,80 @@
|
||||
"""The registry credential (monky-design-docs doc 24 §4a).
|
||||
|
||||
The bundle's manifest carries one entry marked `use: registry-auth` — the estate-wide read-only
|
||||
Harbor robot, copied by monky-tenancy into this environment's own prefix so the agent can read it
|
||||
with the OpenBao policy it already has. It is NOT an env var (an entry in `.env` would put the
|
||||
registry password into every container's environment), so it never reaches the compose file: it is
|
||||
written to a Docker config **the agent owns**, and `DOCKER_CONFIG` points the docker CLI at it.
|
||||
|
||||
That last part is the whole point. The unit runs as `monky-deployd`, whose home is the state dir,
|
||||
so a `docker login` performed by a human or by root is invisible to it — the failure looks exactly
|
||||
like "no credentials at all" (env-dev-01, 2026-09-08).
|
||||
|
||||
Accepted shapes for the secret's value, because the seeded robot has been written both ways:
|
||||
* a JSON object: `{"registry": …, "username": …, "password": …}`
|
||||
* a `username:password` string, with the registry taken from `registry_host` config
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger("monky-deployd.registry")
|
||||
|
||||
USE = "registry-auth"
|
||||
|
||||
|
||||
class RegistryAuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegistryAuth:
|
||||
registry: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
def docker_config(self) -> dict:
|
||||
token = base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
|
||||
return {"auths": {self.registry: {"auth": token}}}
|
||||
|
||||
|
||||
def parse(value: str, *, default_registry: str) -> RegistryAuth:
|
||||
"""`value` is whatever the KV entry held; never logged, never echoed."""
|
||||
text = (value or "").strip()
|
||||
if not text:
|
||||
raise RegistryAuthError("empty registry credential")
|
||||
if text.startswith("{"):
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RegistryAuthError("registry credential is not valid JSON") from exc
|
||||
user, pw = data.get("username"), data.get("password")
|
||||
registry = data.get("registry") or default_registry
|
||||
if not user or not pw:
|
||||
raise RegistryAuthError("registry credential JSON needs username + password")
|
||||
return RegistryAuth(registry=str(registry), username=str(user), password=str(pw))
|
||||
if ":" not in text:
|
||||
raise RegistryAuthError("registry credential is neither JSON nor username:password")
|
||||
user, _, pw = text.partition(":")
|
||||
if not default_registry:
|
||||
raise RegistryAuthError("username:password credential needs a configured registry host")
|
||||
return RegistryAuth(registry=default_registry, username=user, password=pw)
|
||||
|
||||
|
||||
def write_docker_config(dir_path: Path, auth: RegistryAuth) -> Path:
|
||||
"""0600 `config.json` in a directory the agent owns; DOCKER_CONFIG points the CLI at it."""
|
||||
dir_path = Path(dir_path)
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
dir_path.chmod(0o700)
|
||||
target = dir_path / "config.json"
|
||||
tmp = dir_path / "config.json.tmp"
|
||||
tmp.write_text(json.dumps(auth.docker_config(), indent=2) + "\n")
|
||||
tmp.chmod(0o600)
|
||||
tmp.replace(target)
|
||||
log.info("registry credential in place for %s (%s)", auth.registry, auth.username)
|
||||
return target
|
||||
Reference in New Issue
Block a user