Files
acdl/core/onboarding.py
T
Jon Chery fe312c6292 verify(P19): onboarding-envfile-autogen — 4-layer verify PASS + ship
VERIFY: structural — onboarding.py + rebranded message; behavioral — 18 tests + CI PASS; quality — self-service request path (no human handoff).

---ci---
project: acdl
phase: 19
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-183]
  partial: []
---/ci---
2026-08-01 13:29:30 +00:00

131 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Nova Onboarding — auto-generate an environment binding file (P19, REQ-183).
Given a consumer onboarding request (validated against
schemas/onboarding.schema.json), generate a ``<env>.json`` environment
binding file from the dev template, filling in the consumer's ownerId +
billingTag. The generated file is a starting point for the platform team
(or a future automation) to bind to a real AWS account.
This is the "request path" half of the no-humans onboarding flow (D-113).
Real AWS account/network/state provisioning is a future feature milestone;
this module removes the human handoff from the *request* step by
generating the binding file + emitting a git patch / PR-branch instruction.
Usage:
python3 core/onboarding.py <request.json> [--out <env.json>]
python3 core/onboarding.py --request '{"consumerRepo":"acdl/c","requestedEnvironment":"qa","ownerId":"team-a","billingTag":"cc-a"}'
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict
def _repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _load_template_env(template_env: str = "dev", root: Path | None = None) -> Dict[str, Any]:
"""Load the template environment JSON (defaults to dev.json)."""
root = root or _repo_root()
env_path = root / "core" / "environments" / f"{template_env}.json"
if not env_path.is_file():
raise FileNotFoundError(f"template environment {env_path} not found")
return json.loads(env_path.read_text())
def generate_env_file(
request: Dict[str, Any],
template_env: str = "dev",
root: Path | None = None,
) -> Dict[str, Any]:
"""Generate an environment binding dict from a consumer onboarding request.
The generated dict is a copy of the template env with:
- ``name`` → the requested environment
- ``description`` → notes the consumer + owner
- ``account_id`` → placeholder (000000000000) for the platform team
to fill with the real account
- ``ownerId`` + ``billingTag`` → from the request (for ABAC + cost)
The dict validates against schemas/environment.schema.json.
Returns the generated env dict.
"""
template = _load_template_env(template_env, root)
requested = request["requestedEnvironment"]
owner = request["ownerId"]
billing = request["billingTag"]
consumer = request["consumerRepo"]
env = dict(template)
env["name"] = requested
env["description"] = (
f"Auto-generated binding for {consumer} (owner={owner}, "
f"billing={billing}). Replace account_id with the real "
f"{requested} account before deploying."
)
env["account_id"] = "000000000000" # placeholder — platform team fills
env["ownerId"] = owner
env["billingTag"] = billing
return env
def _onboarding_request_message(env_name: str) -> str:
"""P19 (REQ-183): the rebranded Nova onboarding message — self-service
request path, no longer routes to 'contact the platform team'."""
return (
"=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n"
"Nova environments are platform-managed. The platform provisions on\n"
"your behalf:\n"
" - an AWS account (or a scoped partition of one)\n"
" - a network (VPC + subnets)\n"
" - a state backend (an S3 bucket + DynamoDB lock table)\n"
" - an IAM role surfaced to your repo via attribute-based\n"
" authorization (ABAC)\n\n"
"You do not provide an AWS account, VPC, subnet, or state bucket.\n\n"
"To request an environment (self-service):\n"
" 1. Submit an onboarding request to the Nova Lambda\n"
" (action: onboard_consumer) with your repo name + the\n"
" environment name you need (e.g. 'dev').\n"
" 2. The platform generates an environment binding + opens a PR.\n"
" 3. The platform provisions the account/network/state/role and\n"
" grants the ABAC role. Your next pipeline run proceeds.\n\n"
"Run: python3 core/onboarding.py --request '{...}' to generate a\n"
"binding file locally, or POST to the Lambda onboard_consumer action.\n"
"===================================\n"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Generate an env binding from an onboarding request.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("request_file", nargs="?", help="path to a request JSON file")
group.add_argument("--request", help="inline request JSON string")
parser.add_argument("--out", help="output path for the generated env JSON (default: stdout)")
parser.add_argument("--template-env", default="dev", help="template environment (default: dev)")
args = parser.parse_args(argv)
if args.request:
request = json.loads(args.request)
else:
request = json.loads(Path(args.request_file).read_text())
env = generate_env_file(request, template_env=args.template_env)
env_json = json.dumps(env, indent=2) + "\n"
if args.out:
Path(args.out).write_text(env_json)
print(f"wrote: {args.out}")
else:
print(env_json)
return 0
if __name__ == "__main__":
sys.exit(main())