Cut forgejo package prune over to OpenBao lane
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 5s

This commit is contained in:
codex 2026-07-26 09:32:08 +02:00
parent ba767d5edb
commit 12903e3bed
5 changed files with 240 additions and 46 deletions

View file

@ -24,6 +24,9 @@ DEFAULT_OWNER = "coulomb"
DEFAULT_TYPES = ("container", "pypi", "npm", "generic")
DEFAULT_MAX_VERSIONS = 3
DEFAULT_APPS_ROOT = Path.home() / "railiance-apps"
DEFAULT_FORGEJO_ADMIN_BAO_PATH = "platform/workloads/forgejo/forgejo-admin"
DEFAULT_FORGEJO_ADMIN_BAO_FIELD = "API_TOKEN"
LEGACY_FORGEJO_TOKEN_FILE = Path("/tmp/forgejo-tegwick-api-token")
FORGEJO_IMAGE_RE = re.compile(
r"^forgejo\.coulomb\.social/(?:coulomb/)?(?P<name>[^:/]+)(?::(?P<tag>[^/\s]+))?$",
re.IGNORECASE,
@ -323,25 +326,97 @@ def build_delete_plans(
return plans, errors
def _read_token_file(path: Path) -> str:
return path.read_text(encoding="utf-8").strip()
def _truthy_env(name: str) -> bool:
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
def _load_token_from_file_env() -> str | None:
for env_name in ("FORGEJO_TOKEN_FILE", "FORGEJO_ADMIN_TOKEN_FILE"):
raw_path = os.environ.get(env_name, "").strip()
if not raw_path:
continue
path = Path(raw_path).expanduser()
if not path.is_file():
raise SystemExit(f"ERROR: {env_name} points to a missing file: {path}")
token = _read_token_file(path)
if token:
return token
raise SystemExit(f"ERROR: {env_name} points to an empty file: {path}")
return None
def _load_token_from_openbao() -> tuple[str | None, str | None]:
bao_bin = os.environ.get("FORGEJO_ADMIN_BAO_CLI", "bao").strip() or "bao"
bao_path = (
os.environ.get("FORGEJO_ADMIN_BAO_PATH", DEFAULT_FORGEJO_ADMIN_BAO_PATH).strip()
or DEFAULT_FORGEJO_ADMIN_BAO_PATH
)
bao_field = (
os.environ.get("FORGEJO_ADMIN_BAO_FIELD", DEFAULT_FORGEJO_ADMIN_BAO_FIELD).strip()
or DEFAULT_FORGEJO_ADMIN_BAO_FIELD
)
if shutil.which(bao_bin) is None:
return None, f"{bao_bin} CLI not found"
try:
result = subprocess.run(
[bao_bin, "kv", "get", f"-field={bao_field}", bao_path],
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError as exc:
detail = exc.stderr.strip() or exc.stdout.strip() or f"exit {exc.returncode}"
return None, f"{bao_bin} kv get failed: {detail}"
except OSError as exc:
return None, f"{bao_bin} invocation failed: {exc}"
token = result.stdout.strip()
if not token:
return None, f"{bao_bin} kv get returned an empty {bao_field} field"
return token, None
def _token_help_message(bao_error: str | None) -> str:
lines = [
"ERROR: Forgejo API token required (read:package + write:package).",
" Primary: bao login -method=oidc -path=netkingdom role=forgejo-admin-workload-kv-read",
f" Default lane: {DEFAULT_FORGEJO_ADMIN_BAO_PATH} field {DEFAULT_FORGEJO_ADMIN_BAO_FIELD}",
" Override lane via FORGEJO_ADMIN_BAO_PATH / FORGEJO_ADMIN_BAO_FIELD if needed.",
" Break-glass: set FORGEJO_TOKEN / FORGEJO_ADMIN_TOKEN, or set FORGEJO_TOKEN_FILE / FORGEJO_ADMIN_TOKEN_FILE.",
" Legacy /tmp fallback stays opt-in only via FORGEJO_ALLOW_LEGACY_FILE_FALLBACK=1.",
" See: railiance-platform/docs/forgejo-package-prune.md",
]
if bao_error:
lines.insert(3, f" OpenBao lookup failed: {bao_error}")
return "\n".join(lines)
def load_token() -> str:
for env_name in ("FORGEJO_TOKEN", "FORGEJO_ADMIN_TOKEN"):
token = os.environ.get(env_name, "").strip()
if token:
return token
token_file = os.environ.get(
"FORGEJO_TOKEN_FILE",
os.environ.get("FORGEJO_ADMIN_TOKEN_FILE", "/tmp/forgejo-tegwick-api-token"),
)
path = Path(token_file).expanduser()
if path.is_file():
return path.read_text(encoding="utf-8").strip()
raise SystemExit(
"ERROR: Forgejo API token required (read:package + write:package).\n"
" export FORGEJO_TOKEN=<pat> # or FORGEJO_ADMIN_TOKEN\n"
" # or save PAT to /tmp/forgejo-tegwick-api-token (chmod 600)\n"
" Generate: https://forgejo.coulomb.social/user/settings/applications\n"
" See: railiance-platform/docs/forgejo-package-prune.md"
)
token = _load_token_from_file_env()
if token:
return token
token, bao_error = _load_token_from_openbao()
if token:
return token
if _truthy_env("FORGEJO_ALLOW_LEGACY_FILE_FALLBACK"):
if LEGACY_FORGEJO_TOKEN_FILE.is_file():
token = _read_token_file(LEGACY_FORGEJO_TOKEN_FILE)
if token:
return token
suffix = f"legacy file {LEGACY_FORGEJO_TOKEN_FILE} is empty"
else:
suffix = f"legacy file {LEGACY_FORGEJO_TOKEN_FILE} is missing"
bao_error = f"{bao_error}; {suffix}" if bao_error else suffix
raise SystemExit(_token_help_message(bao_error))
def emit_summary(
@ -519,4 +594,4 @@ def main(argv: list[str] | None = None) -> int:
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())