"""Kept-running manifestation of the lifecycle library. This is the same library the CLI uses. The process stays in the job loop until the job stream ends. It is not a second implementation and not a cluster workload manifest. """ from __future__ import annotations import json import os from collections.abc import Iterable, Iterator, Mapping from typing import Any from secrets_engine import lifecycle as lifecycle_lib from secrets_engine.stage_binding import StartupError, load_stage_binding LOCAL_ALLOW_ENV = "SECRETS_ENGINE_LOCAL_ALLOW" CATALOG_STAGE = {"build": "build", "test": "test", "production": "prod"} def local_allow_requested(flag: bool = False) -> bool: """True when a caller asked to be allowed. The answer is not authority.""" env = os.environ.get(LOCAL_ALLOW_ENV, "").strip() in {"1", "true", "yes"} return bool(flag) or env def service_jobs( catalog_id: str, operation: str, lines: Iterable[str] ) -> Iterator[dict[str, str]]: """Yield the argv job, then block on further lines until the stream ends.""" yield {"catalog_id": catalog_id, "operation": operation, "confirm_destroy": ""} for line in lines: text = line.strip() if not text: continue try: payload = json.loads(text) except json.JSONDecodeError as exc: raise StartupError("service job is not a JSON object") from exc if not isinstance(payload, dict): raise StartupError("service job must be a mapping") next_id = str(payload.get("catalog_id") or "") next_operation = str(payload.get("operation") or "") if not next_id or not next_operation: raise StartupError("service job is missing catalog_id or operation") yield { "catalog_id": next_id, "operation": next_operation, "confirm_destroy": str(payload.get("confirm_destroy") or ""), } def run_service( cfg: Any, jobs: Iterable[Mapping[str, str]], *, allow: bool = False, auth_args: Any = None, ) -> list[tuple[Any, Any]]: """Run lifecycle jobs until the iterator ends. Startup reads the stage credential files. Two files refuse startup before any job is pulled. Each job calls ``build_lifecycle_plan`` and then the live lifecycle function the CLI uses, which applies the plan. ``allow`` is discarded and does not authorize that call. """ local_allow_requested(allow) binding = load_stage_binding() if binding is None or not binding.credential or not binding.stage: raise StartupError("startup refused: no stage credential is present") catalog_stage = CATALOG_STAGE.get(binding.stage, "") from types import SimpleNamespace from secrets_engine.catalog import get_entry from secrets_engine.cli import _privileged_evidence, execute_lifecycle_plan outcomes: list[tuple[Any, Any]] = [] for job in jobs: entry = get_entry(cfg.catalog_dir, str(job.get("catalog_id") or "")) if entry.stage != catalog_stage: raise StartupError( "startup refused: lane stage does not match the injected stage" ) operation = str(job.get("operation") or "") plan = lifecycle_lib.build_lifecycle_plan(entry, operation) call = SimpleNamespace( operation=operation, auth=getattr(auth_args, "auth", "env"), bootstrap_token_file=getattr(auth_args, "bootstrap_token_file", None), confirm_destroy=str(job.get("confirm_destroy") or ""), ) with _privileged_evidence( cfg, entry, f"lifecycle-{operation}", detail={"operation": operation}, ) as evidence: result = execute_lifecycle_plan(cfg, call, entry, plan, evidence) outcomes.append((plan, result)) return outcomes