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
This commit is contained in:
parent
06dfe12c18
commit
f211525f00
4 changed files with 299 additions and 151 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue