Manifest the lifecycle engine as a library, a one-stage CLI, and an in-process service.
Planning stays off the store client. The process binds one stage credential at start, serve runs the same lifecycle library, and a local allow flag does not authorize a protected call. Assistant: grok Assistant-Session: 01a1609f-72b8-7222-9dff-fae300115e42
This commit is contained in:
parent
3e3f8fc629
commit
06dfe12c18
8 changed files with 530 additions and 26 deletions
|
|
@ -57,6 +57,7 @@ from secrets_engine.lane_state import (
|
|||
)
|
||||
from secrets_engine.pep_stance import apply_unreachable_engine_stance, with_decision
|
||||
from secrets_engine.openbao import OpenBaoClient, accessor_fingerprint, read_strict_token_file
|
||||
from secrets_engine.stage_binding import StartupError, load_stage_binding
|
||||
from secrets_engine.plan import build_plan
|
||||
from secrets_engine.provision import provision_from_file, provision_generated
|
||||
from secrets_engine.routing import route_lane
|
||||
|
|
@ -281,7 +282,47 @@ def cmd_plan(cfg: Config, args) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def cmd_instance(cfg: Config, args) -> int:
|
||||
"""Report the one stage bound at process start. Never prints the credential."""
|
||||
del cfg
|
||||
binding = getattr(args, "stage_binding", None)
|
||||
if binding is None:
|
||||
raise StartupError("startup refused: no stage credential is present")
|
||||
print(binding.public())
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_serve(cfg: Config, args) -> int:
|
||||
"""Keep the lifecycle library running for the one injected stage."""
|
||||
from secrets_engine.catalog import get_entry
|
||||
from secrets_engine.service import local_allow_requested, run_service
|
||||
|
||||
local_allow_requested(bool(getattr(args, "allow", False)))
|
||||
binding = getattr(args, "stage_binding", None)
|
||||
entry = get_entry(cfg.catalog_dir, args.catalog_id)
|
||||
foreign = getattr(args, "foreign_stage_credential_file", None) or ""
|
||||
foreign_value = ""
|
||||
if foreign:
|
||||
foreign_value = "present"
|
||||
plans = run_service(
|
||||
binding,
|
||||
[
|
||||
{
|
||||
"entry": entry,
|
||||
"operation": args.operation,
|
||||
"foreign_stage_credential": foreign_value,
|
||||
}
|
||||
],
|
||||
)
|
||||
for plan in plans:
|
||||
print(plan.render())
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_apply(cfg: Config, args) -> int:
|
||||
from secrets_engine.service import local_allow_requested
|
||||
|
||||
local_allow_requested(bool(getattr(args, "allow", False)))
|
||||
entry = _resolve_lane_and_decision(cfg, args.ref, args.stage)
|
||||
if args.dry_run:
|
||||
decision = None
|
||||
|
|
@ -1064,6 +1105,34 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
dinsp.add_argument("ref")
|
||||
dinsp.set_defaults(func=cmd_decision_inspect)
|
||||
|
||||
inst = sub.add_parser(
|
||||
"instance",
|
||||
help="print the one stage bound at startup; never prints the credential",
|
||||
)
|
||||
inst.set_defaults(func=cmd_instance)
|
||||
|
||||
srv = sub.add_parser(
|
||||
"serve",
|
||||
help="run the lifecycle library for the one stage injected at startup",
|
||||
)
|
||||
srv.add_argument("catalog_id")
|
||||
srv.add_argument(
|
||||
"--operation",
|
||||
required=True,
|
||||
choices=("suspend", "deactivate", "destroy"),
|
||||
)
|
||||
srv.add_argument(
|
||||
"--allow",
|
||||
action="store_true",
|
||||
help="ignored; a local flag does not authorize this process",
|
||||
)
|
||||
srv.add_argument(
|
||||
"--foreign-stage-credential-file",
|
||||
default="",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
srv.set_defaults(func=cmd_serve)
|
||||
|
||||
pl = sub.add_parser("plan", help="render a guarded plan (no mutation)")
|
||||
pl.add_argument("ref", help="decision/ccr id or catalog id")
|
||||
pl.add_argument("--stage", required=True, choices=("build", "test", "prod"))
|
||||
|
|
@ -1073,6 +1142,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
ap.add_argument("ref")
|
||||
ap.add_argument("--stage", required=True, choices=("build", "test", "prod"))
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
ap.add_argument(
|
||||
"--allow",
|
||||
action="store_true",
|
||||
help="ignored; a local flag does not authorize apply",
|
||||
)
|
||||
add_token_arg(ap)
|
||||
ap.set_defaults(func=cmd_apply)
|
||||
|
||||
|
|
@ -1273,8 +1347,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
try:
|
||||
binding = load_stage_binding()
|
||||
except StartupError as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
return e.exit_code
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
args.stage_binding = binding
|
||||
# `exec` REMAINDER includes a leading '--'; strip it.
|
||||
if getattr(args, "command", None) and args.command and args.command[0] == "--":
|
||||
args.command = args.command[1:]
|
||||
|
|
|
|||
71
src/secrets_engine/service.py
Normal file
71
src/secrets_engine/service.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""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.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable, 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
|
||||
|
||||
LOCAL_ALLOW_ENV = "SECRETS_ENGINE_LOCAL_ALLOW"
|
||||
|
||||
|
||||
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 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,
|
||||
*,
|
||||
allow: bool = False,
|
||||
auth_args: Any = None,
|
||||
) -> Any:
|
||||
"""Gate a protected call the way the CLI does.
|
||||
|
||||
``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.
|
||||
"""
|
||||
local_allow_requested(allow)
|
||||
from secrets_engine.cli import _require_lane_approval
|
||||
|
||||
return _require_lane_approval(
|
||||
cfg, entry, action, None, auth_args=auth_args
|
||||
)
|
||||
60
src/secrets_engine/stage_binding.py
Normal file
60
src/secrets_engine/stage_binding.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""One stage credential, injected at process start.
|
||||
|
||||
The process may hold the credential for exactly one of build, test, or
|
||||
production. The value stays in memory. Startup errors name the condition and
|
||||
never include the credential.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from secrets_engine.errors import SecretsEngineError
|
||||
|
||||
STAGE_TOKEN_FILES = {
|
||||
"build": "SECRETS_ENGINE_STAGE_TOKEN_FILE_BUILD",
|
||||
"test": "SECRETS_ENGINE_STAGE_TOKEN_FILE_TEST",
|
||||
"production": "SECRETS_ENGINE_STAGE_TOKEN_FILE_PRODUCTION",
|
||||
}
|
||||
|
||||
|
||||
class StartupError(SecretsEngineError):
|
||||
"""Process start refused before a command runs."""
|
||||
|
||||
exit_code = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StageBinding:
|
||||
stage: str
|
||||
credential: str
|
||||
|
||||
def public(self) -> str:
|
||||
return f"stage={self.stage}"
|
||||
|
||||
|
||||
def load_stage_binding(environ: dict[str, str] | None = None) -> StageBinding | None:
|
||||
"""Read at most one stage credential file. None means the caller set none."""
|
||||
env = os.environ if environ is None else environ
|
||||
present: list[tuple[str, str]] = []
|
||||
for stage, key in STAGE_TOKEN_FILES.items():
|
||||
raw = env.get(key, "").strip()
|
||||
if raw:
|
||||
present.append((stage, raw))
|
||||
if len(present) > 1:
|
||||
raise StartupError(
|
||||
"startup refused: more than one stage credential is present"
|
||||
)
|
||||
if not present:
|
||||
return None
|
||||
stage, path_text = present[0]
|
||||
try:
|
||||
credential = Path(path_text).read_text(encoding="utf-8").strip()
|
||||
except OSError as exc:
|
||||
raise StartupError(
|
||||
"startup refused: stage credential file is unreadable"
|
||||
) from exc
|
||||
if not credential:
|
||||
raise StartupError("startup refused: stage credential file is empty")
|
||||
return StageBinding(stage=stage, credential=credential)
|
||||
Loading…
Add table
Add a link
Reference in a new issue