secrets-engine/src/secrets_engine/service.py

101 lines
3.8 KiB
Python
Raw Normal View History

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