fe312c6292
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---
131 lines
5.3 KiB
Python
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()) |