secrets-engine/src/secrets_engine/service.py
tegwick f211525f00
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Keep serve on the live lifecycle path until the job stream ends.
The process reads one stage credential file at startup, applies each job through the CLI lifecycle functions, and waits for further jobs. A second stage credential file refuses startup. A local allow flag does not authorize the call.

Assistant: grok
Assistant-Session: 01a1609f-72b8-7222-9dff-fae300115e42
2026-10-22 18:15:23 +02:00

100 lines
3.8 KiB
Python

"""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