Keep serve on the live lifecycle path until the job stream ends.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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
This commit is contained in:
tegwick 2026-10-22 18:15:23 +02:00
parent 06dfe12c18
commit f211525f00
4 changed files with 299 additions and 151 deletions

View file

@ -1,20 +1,21 @@
"""Kept-running manifestation of the lifecycle library.
This is the same library the CLI uses, in a process that stays up for one
injected stage. It is not a second implementation and not a Deployment.
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, Mapping
from collections.abc import Iterable, Iterator, Mapping
from typing import Any
from secrets_engine import lifecycle as lifecycle_lib
from secrets_engine.catalog import CatalogEntry
from secrets_engine.lifecycle import LifecyclePlan
from secrets_engine.stage_binding import StageBinding, StartupError
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:
@ -23,49 +24,77 @@ def local_allow_requested(flag: bool = False) -> bool:
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(
binding: StageBinding | None,
jobs: Iterable[Mapping[str, Any]],
) -> list[LifecyclePlan]:
"""Run lifecycle jobs for one stage until the job iterable ends.
A job may carry ``foreign_stage_credential`` only as a refusal test. A
present value refuses the job. The binding credential is never rendered.
"""
if binding is None or not binding.credential or not binding.stage:
raise StartupError("startup refused: no stage credential is present")
plans: list[LifecyclePlan] = []
for job in jobs:
foreign = str(job.get("foreign_stage_credential") or "")
if foreign:
raise StartupError(
"startup refused: another stage credential is present"
)
entry = job.get("entry")
if not isinstance(entry, CatalogEntry):
raise StartupError("service job is missing a catalog entry")
operation = str(job.get("operation") or "")
plans.append(lifecycle_lib.build_lifecycle_plan(entry, operation))
return plans
def invoke_protected(
cfg: Any,
entry: CatalogEntry,
action: str,
jobs: Iterable[Mapping[str, str]],
*,
allow: bool = False,
auth_args: Any = None,
) -> Any:
"""Gate a protected call the way the CLI does.
) -> list[tuple[Any, Any]]:
"""Run lifecycle jobs until the iterator ends.
``allow`` and ``SECRETS_ENGINE_LOCAL_ALLOW`` are recorded as not authority
and then discarded. The call still requires a decision record or the
published unreachable-engine stance.
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)
from secrets_engine.cli import _require_lane_approval
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
return _require_lane_approval(
cfg, entry, action, None, auth_args=auth_args
)
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