Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8cf46b94c | |||
| fe9ab7e444 | |||
| 2443909362 |
@@ -0,0 +1,116 @@
|
||||
# Nova Platform — Architecture
|
||||
|
||||
> Simplified from the Nova reference. The reference's six cross-cutting
|
||||
> concerns (security, policy, confidence, outbox/audit, identity, CI
|
||||
> pipeline contract) are removed. What remains is the four-layer
|
||||
> infrastructure-delivery core.
|
||||
|
||||
## Layers (4)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 1. Contract Surface schemas/contract.schema.json
|
||||
│ contracts/*.yaml (samples)
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 2. Resolution core/contract_resolver.py
|
||||
│ core/environment_check.py
|
||||
│ schemas/stack.schema.json
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 3. Engine Adapter adapters/terraform/ (the only
|
||||
│ (only engine-specific) engine-specific code)
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 4. Apply terraform/ (bootstrap, modules)
|
||||
│ scripts/run_platform.sh
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer 1 — Contract Surface
|
||||
|
||||
A consumer writes a small YAML contract:
|
||||
|
||||
```yaml
|
||||
id: stsi
|
||||
name: My Static Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: my-static-site-assets
|
||||
index_document: index.html
|
||||
```
|
||||
|
||||
Validated against `schemas/contract.schema.json`. The contract is the
|
||||
only consumer-facing surface. It is engine-agnostic — no `aws_*` terms.
|
||||
|
||||
### Layer 2 — Resolution
|
||||
|
||||
`core/contract_resolver.py` resolves a validated contract to a Stack
|
||||
instance (a typed structure conforming to `schemas/stack.schema.json`).
|
||||
Resolution is pure: contract in, stack out. No I/O, no engine terms.
|
||||
|
||||
`core/environment_check.py` validates that the named environment exists
|
||||
in `core/environments/*.json` and that the caller is permitted to use
|
||||
it. Environments are platform-managed (consumers provide no AWS account,
|
||||
VPC, or state bucket).
|
||||
|
||||
### Layer 3 — Engine Adapter
|
||||
|
||||
`adapters/terraform/` is the only engine-specific code. It takes a Stack
|
||||
and emits Terraform (`module "x" { source = "../../modules/..." }`
|
||||
blocks). The adapter is a stateless assembler — lifecycle ownership
|
||||
belongs to Terraform via the shell orchestrator. This is the only place
|
||||
`aws_*` / Terraform terms appear.
|
||||
|
||||
### Layer 4 — Apply
|
||||
|
||||
`scripts/run_platform.sh` orchestrates: contract → resolve → adapter →
|
||||
`terraform init` → `terraform plan` → `terraform apply`. Modes:
|
||||
`--check-only` (offline, structure validation), `--plan-only` (no
|
||||
apply), full (apply). `--quiet` suppresses streaming.
|
||||
|
||||
## Engine Boundary (Enforced)
|
||||
|
||||
The engine boundary is strict. Code outside `adapters/terraform/` MUST
|
||||
NOT contain engine-specific terms (`aws_s3_bucket`, `aws_*`, Terraform
|
||||
HCL). This invariant is verified by tests (`tests/test_engine_boundary.py`).
|
||||
|
||||
## What is NOT here (intentionally removed vs the reference)
|
||||
|
||||
- No `core/confidence_signal.py` — no score gating apply.
|
||||
- No `core/outbox_writer.py` — no hash-chained evidence events.
|
||||
- No `core/policy_engine.py` / `adapters/kyverno-json/` /
|
||||
`adapters/wiz/` — no policy checks.
|
||||
- No `core/abac_evaluator.py` / `core/auth_store.py` /
|
||||
`core/jws_attestation.py` / `core/kms_signing.py` /
|
||||
`core/pat_lifecycle.py` / `core/separation_of_duties.py` /
|
||||
`core/hitl_gates.py` / `core/attestation_matrix.py` /
|
||||
`core/submission_readiness.py` — no identity/ABAC/HITL.
|
||||
- No `adapters/checkov/` custom rules — no Checkov.
|
||||
- No `.github/workflows/` — no CI pipeline (local shell only).
|
||||
- No `pipelines/` — no central pipeline contract.
|
||||
- No `schemas/pipeline.schema.json` /
|
||||
`schemas/deploy-pipeline.schema.json` /
|
||||
`schemas/policy_check_result.schema.json` /
|
||||
`schemas/metrics_*.schema.json` — those schemas are dropped.
|
||||
- No `metrics/` — no platform telemetry.
|
||||
- No `core/regression_verify*.py` / `core/metrics/` — no regression or
|
||||
metrics modules.
|
||||
- No leadership decks, PPTX rendering, marp slides.
|
||||
- No `core/env_transition.py` / `core/decommission_transform.py` /
|
||||
`core/mode_resolver.py` / `core/onboarding.py` — no env transition,
|
||||
decommission, mode resolution, or onboarding flow beyond bootstrap.
|
||||
|
||||
## Module Catalog
|
||||
|
||||
L1 primitives (single resources) + L2 patterns (composites of
|
||||
primitives). Each module has an `interface.json` (inputs/outputs, no
|
||||
engine terms) and a `terraform/` directory. `modules/registry.json`
|
||||
indexes every module + version.
|
||||
|
||||
**L1 (primitives):** s3, vpc, ecs-cluster, ecs-service, iam-role, alb,
|
||||
ecr, cloudfront, waf, rds, kms-key, dynamodb, uptime.
|
||||
|
||||
**L2 (patterns):** microservice (vpc + ecs-cluster + ecs-service +
|
||||
iam-role + ecr + alb — locked D-038), static-assets (s3 + cloudfront +
|
||||
kms-key — locked D-038, drops waf from reference).
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "specify",
|
||||
"milestone": "v1.1",
|
||||
"phase_role": "pre_execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-28T19:09:26Z",
|
||||
"project": "nova-platform",
|
||||
"milestone_branch": "milestone/v1.1-pdlc-state",
|
||||
"phase_branch": "phase/00-pre-execution",
|
||||
"milestone_complete": false,
|
||||
"pdlc_intake": true,
|
||||
"state_md_created": true
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
# CLARIFY — Nova Platform v1.0 (Phase 0)
|
||||
|
||||
> 25 decisions (D-011..D-035) resolving remaining specification
|
||||
> ambiguities. Init already locked D-001..D-010. Autonomy: supervised
|
||||
> (present + wait for human review).
|
||||
|
||||
## Architecturally significant (highest impact)
|
||||
|
||||
### D-011: Resolver purity vs file I/O
|
||||
`resolve()` is declared pure (no I/O) in REQ-03 but must load
|
||||
`interface.json` / composition data from disk.
|
||||
- **Default:** Add `modules_dir: Path` as 3rd param. "No I/O" means no
|
||||
network/side-effects — local file reads for module metadata are
|
||||
permitted. The registry provides paths; the resolver reads the files.
|
||||
- Confidence: 0.82
|
||||
- Alternatives: [pre-enrich registry inline (truly pure); caller
|
||||
pre-loads interfaces as 4th dict; take contract_path string like ref]
|
||||
|
||||
### D-012: L2 representation — opaque vs expanded
|
||||
Does the resolver expand L2 compositions into individual L1 stack
|
||||
resources, or treat L2 as a single opaque resource?
|
||||
- **Default:** L2 is a single opaque resource. Stack has one entry:
|
||||
`{module:"microservice", version, source:"modules/l2/microservice/terraform",
|
||||
inputs}`. The L2's `terraform/main.tf` composes L1 internally via
|
||||
`module` blocks. Resolver does NOT expand children.
|
||||
- Confidence: 0.88
|
||||
- Alternatives: [keep reference expansion (children/wires, needs richer
|
||||
stack schema — contradicts REQ-04's flat shape); hybrid]
|
||||
|
||||
### D-013: L2 file naming + registry `terraform_dir`
|
||||
REQ-12 says L2 has `interface.json` (reference uses `composition.json`).
|
||||
Reference L2 registry entries omit `terraform_dir` but the flat stack
|
||||
needs `source`.
|
||||
- **Default:** L2 uses `interface.json` (L2-level inputs/outputs, no
|
||||
children/wires — those are in terraform/main.tf). L2 registry entries
|
||||
DO include `terraform_dir: "modules/l2/<name>/terraform"` (deviation
|
||||
from ref L2 entries which omit it — required by flat stack `source`).
|
||||
`kind: "l2"` retained.
|
||||
- Confidence: 0.80
|
||||
- Alternatives: [keep `composition.json` name but simplified content;
|
||||
keep ref L2 shape exactly]
|
||||
|
||||
## Contract / schema details
|
||||
|
||||
### D-015: Contract field constraints
|
||||
- **Default:** Keep ref `id` pattern `^[a-z][a-z0-9-]{2,5}$`, `name`
|
||||
`minLength:3`. `version` OPTIONAL (defaults to latest non-deprecated).
|
||||
`infrastructure[]` items: `module` (required), `version` (optional,
|
||||
semver `^\d+\.\d+\.\d+$`), `inputs` (required, object,
|
||||
`additionalProperties:false`).
|
||||
- Confidence: 0.85
|
||||
|
||||
### D-016: Interpolation `${env.*}` / `${contract.*}`
|
||||
- **Default:** KEEP interpolation. Resolver expands `${env.<field>}` and
|
||||
`${contract.<field>}` after environment_check loads env JSON. Sample
|
||||
contracts use `${env.region}`, `${env.account_id}` for naming.
|
||||
Unknown tokens raise `ValueError`.
|
||||
- Confidence: 0.78
|
||||
|
||||
### D-017: schemas/environment.schema.json — keep or drop?
|
||||
- **Default:** KEEP but simplify to match reduced field set (D-018).
|
||||
Validates `core/environments/*.json`.
|
||||
- Confidence: 0.70 *(below supervised threshold — escalate)*
|
||||
|
||||
### D-033: Per-env contract variants
|
||||
- **Default:** Variants differ ONLY in `environment` field. All other
|
||||
fields identical. Interpolation resolves per-env at resolver time.
|
||||
- Confidence: 0.85
|
||||
|
||||
### D-035: `index_document` input — L1 s3 or L2 static-assets?
|
||||
- **Default:** `index_document` is an L2 `static-assets` input
|
||||
(passthrough to s3 website config in L2 terraform). L1 `s3` does NOT
|
||||
gain it (stays ref interface: `bucket_name`/`region`/`kms_key_arn`/
|
||||
`enabled`).
|
||||
- Confidence: 0.80
|
||||
|
||||
## Resolver / adapter / interface
|
||||
|
||||
### D-014: L1 `interface.json` field set
|
||||
- **Default:** Keep `name`, `version`, `kind`, `type`, `description`,
|
||||
`inputs`, `outputs`, `resources` (multi-resource array for vpc/
|
||||
ecs-service/alb). Drop `nfrs` (feeds confidence signal — OOS) and
|
||||
`intra_refs` (feeds wire engine — eliminated by D-012).
|
||||
- Confidence: 0.72 *(below threshold — escalate)*
|
||||
|
||||
## Environment model
|
||||
|
||||
### D-018: `core/environments/dev.json` field set
|
||||
- **Default:** Keep `name`, `description`, `account_id`, `region`,
|
||||
`state_backend` (bucket, lock_table), `network` (vpc_cidr, azs).
|
||||
Drop `runner_role_arn` (ABAC OOS), `autonomy` (HITL OOS),
|
||||
`confidence_threshold` (OOS).
|
||||
- Confidence: 0.80
|
||||
|
||||
### D-019: `environment_check.check()` signature
|
||||
- **Default:** Use REQ-05 signature: `check(env_name: str,
|
||||
environments_dir: Path) -> dict`. Raises `EnvironmentNotFoundError`.
|
||||
Drop ref's `(ok, message)` tuple + onboarding prompt printing.
|
||||
- Confidence: 0.90
|
||||
|
||||
## Shell scripts
|
||||
|
||||
### D-020: `run_platform.sh` stages (policy/confidence/outbox dropped)
|
||||
- **Default:**
|
||||
- `--check-only` (offline): env_check → validate contract → resolve →
|
||||
adapter compiles → validate output structure → print
|
||||
`=== PLATFORM CHECK OK ===` → exit 0
|
||||
- `--plan-only` (AWS): above + load creds → terraform init → validate
|
||||
→ plan → print `=== PLATFORM PLAN OK ===`
|
||||
- default (AWS, apply): above + `terraform apply -auto-approve` →
|
||||
print `=== PLATFORM APPLY OK ===`
|
||||
- `--quiet`: suppresses TF streaming. `--help`: flags.
|
||||
- Confidence: 0.85
|
||||
|
||||
### D-021: `run_ci.sh` Stage 1 py_compile
|
||||
- **Default:** Glob: `python3 -m py_compile $(find core/ adapters/
|
||||
scripts/ -name '*.py')`. No hardcoded file list (no OOS Python files
|
||||
exist).
|
||||
- Confidence: 0.82
|
||||
|
||||
### D-031: `run_platform.sh` flag set
|
||||
- **Default:** Keep only REQ-20's four: `--check-only`, `--plan-only`,
|
||||
`--quiet`, `--help` (+`-h`). Default = apply. Drop `--apply`,
|
||||
`--destroy`, `--local`, `--decommission`, `--deploy-uptime`,
|
||||
`--environment`.
|
||||
- Confidence: 0.87
|
||||
|
||||
## Terraform / bootstrap
|
||||
|
||||
### D-022: Bootstrap DynamoDB table
|
||||
- **Default:** Dedicated lock table `nova-tfstate-locks` (S3 backend
|
||||
`lock_table` points to it). NO `nova-outbox` table (outbox OOS). S3
|
||||
state bucket `nova-tfstate-<account>-<region>` with versioning kept.
|
||||
- Confidence: 0.78
|
||||
|
||||
### D-023: `terraform/platform/main.tf` content
|
||||
- **Default:** ONLY shared platform VPC: `aws_vpc.nova_shared`,
|
||||
`aws_subnet.nova_shared` (count=2), IGW, route table, ECS SG, outputs
|
||||
(`vpc_id`, `subnet_ids`, `ecs_security_group_id`). State backend
|
||||
references. Drop Lambda, DynamoDB contracts, KMS, Secrets, SNS,
|
||||
consumer_invoke_policy — all OOS.
|
||||
- Confidence: 0.83
|
||||
|
||||
### D-024: `terraform/ci-vpc/main.tf` role
|
||||
- **Default:** Short-lived test VPC for module lifecycle testing (ref
|
||||
convention preserved). The shared platform VPC lives in
|
||||
`terraform/platform/main.tf` (D-023). REQ-16's description was a
|
||||
mischaracterization.
|
||||
- Confidence: 0.86
|
||||
|
||||
### D-025: Onboarding static-key alternative
|
||||
- **DECISION (human override):** `terraform/onboarding/main.tf` creates
|
||||
an IAM ROLE (not a user) with a trust policy allowing the platform's
|
||||
runner user to assume it (cross-account assume role pattern). No OIDC.
|
||||
`consumer_repo`/`owner_id` vars kept for tagging. README documents
|
||||
this is dev-only static-key (OIDC is production path, OOS for v1.0).
|
||||
The consumer's CI runner assumes this role via `sts assume-role` using
|
||||
the platform runner's static credentials.
|
||||
- Confidence: 0.72 → locked by human review at 1.0
|
||||
|
||||
### D-026: IAM runner policy scoping
|
||||
- **Default:** Static inline policy `terraform/bootstrap/
|
||||
spike_runner_policy.json` scoped to platform account+region, granting
|
||||
Terraform-deployable resource permissions. NOT ABAC-scoped. Account
|
||||
ID parameterized via variable (not hardcoded). Attached to
|
||||
`nova-spike-runner` user by `create_iam_user.py`.
|
||||
- Confidence: 0.75
|
||||
|
||||
## Python packaging / naming / docs / tests
|
||||
|
||||
### D-027: `pyproject.toml` — CLI package or scripts only?
|
||||
- **Default:** No CLI package. `pyproject.toml` configures pytest +
|
||||
py_compile + project metadata. No `[project.scripts]`. No `nova/`
|
||||
package dir. Invoked via shell scripts. Python modules run as
|
||||
scripts.
|
||||
- Confidence: 0.85
|
||||
|
||||
### D-028: `docs/modules/` layout
|
||||
- **Default:** `docs/modules/index.md` (catalog table linking to
|
||||
`modules/l1/<name>/README.md` and `modules/l2/<name>/README.md`).
|
||||
Per-module docs live in `modules/`, not `docs/modules/`. Mirrors ref
|
||||
exactly.
|
||||
- Confidence: 0.82
|
||||
|
||||
### D-029: `modules/STANDARDS.md` + `README-TEMPLATE.md`
|
||||
- **Default:** Keep `modules/README.md` (REQ-13, trimmed of
|
||||
security/attestation). Keep `modules/README-TEMPLATE.md` (per-module
|
||||
template). Drop `modules/STANDARDS.md` (673 lines, mostly
|
||||
security/compliance/attestation — OOS).
|
||||
- Confidence: 0.75
|
||||
|
||||
### D-030: moto usage in tests
|
||||
- **Default:** moto pinned in requirements-test.txt (REQ-31) but used
|
||||
minimally. Check-only is offline (no AWS). Most tests are pure
|
||||
(resolver, adapter, schemas). moto kept for future AWS-touching
|
||||
tests. If unused, harmless pinned dep.
|
||||
- Confidence: 0.68 *(below threshold — escalate)*
|
||||
|
||||
### D-032: `NOVA_*` env var naming
|
||||
- **Default:** Keep `NOVA_*` prefix: `NOVA_AWS_ACCESS_KEY_ID`,
|
||||
`NOVA_AWS_SECRET_ACCESS_KEY`, `NOVA_BOOTSTRAP_AWS_*`, `NOVA_FORGE_TOKEN`.
|
||||
`.env.secrets` uses `NOVA_AWS_*` keys. `run_platform.sh` copies
|
||||
`NOVA_AWS_*` to `AWS_*` env vars.
|
||||
- Confidence: 0.88
|
||||
|
||||
### D-034: Engine-boundary test (REQ-09) file scope
|
||||
- **Default:** Scan `.py` files only (in `core/`, `schemas/`,
|
||||
`contracts/`, `tests/`, `scripts/`, root). Exclude `adapters/terraform/`
|
||||
(the boundary), `modules/`, `.tf`, `.md`, `.json` in modules/envs
|
||||
(data files with `terraform_dir` paths — not engine logic).
|
||||
- Confidence: 0.83
|
||||
|
||||
## Grill fixes (D-037, D-038 — locked from GRILL.md conditions)
|
||||
|
||||
### D-037: Resolver-source / engine-boundary / adapter-signature (C-1 fix)
|
||||
- **DECISION (human-locked):** Match the reference design. Drop `source`
|
||||
from `stack.schema.json` (REQ-04). Change `adapt(stack, modules_dir)`
|
||||
→ `adapt(stack, repo_root)` (REQ-07). The adapter loads `registry.json`
|
||||
internally to map `module` → `terraform_dir` — this happens inside
|
||||
the engine boundary (`adapters/terraform/`), so it's permitted. The
|
||||
resolver never writes Terraform paths into the stack. Stack is
|
||||
engine-agnostic: `{contract_id, contract_name, environment, resources:
|
||||
[{module, version, inputs}]}`. Side effect (C-4): P2 (adapter) gains a
|
||||
dependency on P3-W1 (registry.json) — reorder P3-W1 before P2-W1.
|
||||
- Confidence: 1.0 (human-locked grill fix)
|
||||
- Alternatives: [keep `source` + relax boundary test (fragile); rename
|
||||
to `module_path` (doesn't fix the substring match)]
|
||||
|
||||
### D-038: L2 child set (C-2 fix)
|
||||
- **DECISION (human-locked):** Lock the ARCHITECTURE.md compositions as
|
||||
D-036. `microservice` = vpc + ecs-cluster + ecs-service + iam-role +
|
||||
ecr + alb (6 L1s). `static-assets` = s3 + cloudfront + kms-key (3 L1s,
|
||||
drops waf from reference). These are fresh compositions under the
|
||||
opaque L2 model (D-012) — not reference mirrors.
|
||||
- Confidence: 1.0 (human-locked grill fix)
|
||||
- Alternatives: [match reference children exactly (microservice no
|
||||
vpc, static-assets keeps waf); microservice + kms (7 L1s)]
|
||||
@@ -0,0 +1,115 @@
|
||||
# GRILL — Nova Platform v1.0 (Phase 0)
|
||||
|
||||
> Adversarial red-team of the v1.0 plan. 9 axes reviewed against the
|
||||
> reference at `/home/opencode/acdl/` + locked decisions.
|
||||
|
||||
## Verdict: PROCEED-WITH-CONDITIONS — Confidence 0.82 → PROCEED (all conditions resolved)
|
||||
|
||||
One **blocking** architectural contradiction (C-1) + 5 non-binding
|
||||
conditions. **All resolved under supervised autonomy:**
|
||||
- C-1 (BLOCKING): D-037 locked — adapter loads registry, no `source`
|
||||
in stack. REQ-04 + REQ-07 updated.
|
||||
- C-2 (High): D-038 locked — L2 compositions = ARCHITECTURE.md's.
|
||||
- C-3 (Medium): AC-8 now mechanically checkable (docs OOS grep).
|
||||
- C-4 (Medium): Wave graph reordered — P3-W1 before P2-W1.
|
||||
- C-5 (Low): Concurrency note added to PLAN.md.
|
||||
- C-6 (Low): "without deviation" → "with 9 locked deviations" in
|
||||
PROJECT.md + PERSONAS.md.
|
||||
|
||||
## Binding conditions
|
||||
|
||||
### C-1 (BLOCKING) — resolver-source / engine-boundary / adapter-signature contradiction
|
||||
|
||||
**The kill shot.** Three locked decisions are mutually unsatisfiable:
|
||||
|
||||
| Decision | Requires | Location |
|
||||
|----------|----------|----------|
|
||||
| REQ-04 + D-012 | Stack resource carries `source` field (Terraform path) | `core/contract_resolver.py` populates it |
|
||||
| REQ-09 + D-034 | No `.py` in `core/` may contain `terraform` | `tests/test_engine_boundary.py` greps |
|
||||
| REQ-07 | `adapt(stack, modules_dir) -> str` — no registry param | adapter can't resolve `source` |
|
||||
|
||||
**Verified against reference:** `acdl/schemas/stack.schema.json` has NO
|
||||
`source` field. `acdl/adapters/terraform/adapter.py:19` loads
|
||||
`registry.json` itself (`_load_registry`). The reference resolver never
|
||||
writes a `terraform_dir` path into the stack. nova-platform's design
|
||||
inverts this — putting `source` in the stack (resolver's job) while
|
||||
keeping the adapter signature registry-less. The resolver must write
|
||||
`"modules/l1/s3/terraform"` (contains forbidden `terraform`) → **AC-5
|
||||
+ AC-10 will fail.**
|
||||
|
||||
**Fix (recommended — matches reference):**
|
||||
- Drop `source` from `stack.schema.json` (REQ-04).
|
||||
- Change `adapt(stack, modules_dir)` → `adapt(stack, repo_root)` (REQ-07).
|
||||
- Adapter loads `registry.json` internally (inside the boundary — it's
|
||||
the engine-specific code, permitted to read `terraform_dir`).
|
||||
- L2 `terraform_dir` in registry (D-013) consumed by adapter, not resolver.
|
||||
|
||||
**Side effect (C-4):** if adopted, P2 (adapter) gains a dependency on
|
||||
P3-W1 (registry.json). Reorder: P3-W1 before P2-W1, or split P3.
|
||||
|
||||
### C-2 (High) — L2 child set underspecified
|
||||
|
||||
ARCHITECTURE.md:114-115 describes L2 compositions:
|
||||
- `microservice = vpc + ecs-cluster + ecs-service + iam-role + ecr + alb`
|
||||
- `static-assets = s3 + cloudfront + kms-key`
|
||||
|
||||
But the reference `microservice` children = cluster, ecr, roles, alb,
|
||||
service, kms (no vpc); `static-assets` = s3, cloudfront, **waf**, kms.
|
||||
Nova drops waf from static-assets (consistent — waf L1 kept but not in
|
||||
the L2). These are fresh-authored under D-012 (opaque L2), so not a
|
||||
reference-mirror violation, but the composition is underspecified — no
|
||||
decision locks the L2 child set.
|
||||
|
||||
**Fix:** Lock D-036 before P4-W2 specifying exactly which L1 modules
|
||||
each L2 composes. Reconcile ARCHITECTURE.md.
|
||||
|
||||
### C-3 (Medium) — AC-8 not mechanically checkable
|
||||
|
||||
AC-8 ("docs... no OOS sections") is subjective. P6-W3 greps for OOS
|
||||
*file names* but not OOS *content* inside allowed docs. A doc could
|
||||
contain a "Security" section and pass.
|
||||
|
||||
**Fix:** Add grep check for OOS section headings ("Security",
|
||||
"Compliance", "OIDC", "Attestation", "ABAC") inside `docs/*.md` +
|
||||
`modules/*/README.md`. Make AC-8 mechanically checkable.
|
||||
|
||||
### C-4 (Medium) — missing dep if C-1(a) adopted
|
||||
|
||||
If C-1's recommended fix is adopted (adapter loads registry), P2 gains
|
||||
a dependency on P3-W1 (registry.json). Current graph runs P2 + P3 in
|
||||
parallel after P1.
|
||||
|
||||
**Fix:** Reorder P3-W1 before P2-W1, or split P3 into "registry first"
|
||||
+ "L1 terraform second". Update the wave dependency graph.
|
||||
|
||||
### C-5 (Low) — concurrency cap violation
|
||||
|
||||
PLAN claims "No wave has >5 parallel tasks" but P3-W1 has 13, P3-W2 has
|
||||
13, P4-W1 has 8. `max_concurrent_agents=5` → these batch-serialize into
|
||||
3-5 rounds, inflating P3 wall-clock ~2-3×.
|
||||
|
||||
**Fix:** Either raise the cap for these waves or restate the schedule
|
||||
estimate to reflect batching. Non-blocking.
|
||||
|
||||
### C-6 (Low) — "without deviation" claim is false
|
||||
|
||||
PROJECT.md:11 + PERSONAS.md:17,148 claim "structural conventions
|
||||
preserved **without deviation**." RESEARCH.md itself lists **9 locked
|
||||
deviations** (D-012, D-013, D-015, D-017, D-018, D-019, D-022, D-023,
|
||||
D-025, D-027). An implementer may reject the needed C-1 fix as
|
||||
"violating conventions."
|
||||
|
||||
**Fix:** Rewrite the claim to "structural conventions preserved except
|
||||
the 9 locked deviations in CLARIFY.md."
|
||||
|
||||
## Additional notes (non-binding)
|
||||
|
||||
- **ARCHITECTURE.md:32 example bug:** `id: my-static-site` (14 chars)
|
||||
fails the locked pattern `^[a-z][a-z0-9-]{2,5}$` (max 6). Fix to
|
||||
`id: stsi` or `id: assets`.
|
||||
- **D-014 (0.72), D-017 (0.70), D-030 (0.68):** marked below threshold
|
||||
in CLARIFY.md but D-017/D-030 show no human-lock record (unlike D-025).
|
||||
These WERE escalated + answered in the clarify stage question round —
|
||||
confirmed locked. No action needed.
|
||||
- **moto pinned but unused (D-030):** harmless. Keep or drop; not
|
||||
blocking.
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
personas:
|
||||
- name: lead-developer
|
||||
domain: coordination
|
||||
active: true
|
||||
frameworks:
|
||||
- python
|
||||
- bash
|
||||
- terraform
|
||||
- jsonschema
|
||||
- pytest
|
||||
- boto3
|
||||
constraints:
|
||||
- pragmatic
|
||||
- battle-tested defaults
|
||||
- engine-agnostic core
|
||||
- acdl structural conventions preserved with 9 locked deviations (D-012, D-013, D-015, D-017, D-018, D-019, D-022, D-023, D-025, D-027)
|
||||
- no security/identity/CI-workflow (out of scope)
|
||||
- local shell reproducibility over CI
|
||||
territory:
|
||||
- ".ciagent/**"
|
||||
- "README.md"
|
||||
- "pyproject.toml"
|
||||
- "requirements-test.txt"
|
||||
- ".gitignore"
|
||||
- "docs/architecture.md"
|
||||
- "docs/consumer-guide.md"
|
||||
- name: data-engineer
|
||||
domain: data
|
||||
active: true
|
||||
frameworks:
|
||||
- terraform
|
||||
- hcl
|
||||
- jsonschema
|
||||
- json
|
||||
constraints:
|
||||
- schema-first
|
||||
- type-safe
|
||||
- migration-driven
|
||||
- engine terms only in adapters/terraform/ + modules/*/terraform/ + terraform/
|
||||
- module interface shape: {name, version, kind, type, description, inputs, outputs, resources[]}
|
||||
- registry entry shape: {interface, terraform_dir, published_at, deprecated, kind}
|
||||
- L2 is opaque (D-012): interface.json + terraform/main.tf composes L1 internally
|
||||
- state key convention: spike/<stack_name>/<environment>/terraform.tfstate
|
||||
- tag convention: nova:owner/nova:contract/nova:environment/nova:cost-center
|
||||
territory:
|
||||
- "modules/**"
|
||||
- "terraform/**"
|
||||
- "schemas/stack.schema.json"
|
||||
- "schemas/environment.schema.json"
|
||||
- "docs/modules/**"
|
||||
- "docs/environments/**"
|
||||
- "**/*.tf"
|
||||
- "**/*.tf.json"
|
||||
- name: backend-engineer
|
||||
domain: backend
|
||||
active: true
|
||||
frameworks:
|
||||
- python
|
||||
- pyyaml
|
||||
- jsonschema
|
||||
- boto3
|
||||
- pytest
|
||||
- moto
|
||||
- bash
|
||||
constraints:
|
||||
- api-first
|
||||
- strict-typing
|
||||
- dependency-injection
|
||||
- engine-agnostic core (no aws_*/terraform/module "/provider "/resource " strings outside adapters/terraform/)
|
||||
- resolve() is pure: no network/side-effects; local file reads for module metadata permitted (D-011)
|
||||
- adapt() is a stateless assembler: no terraform invocation, no state files, no plan files
|
||||
- named exceptions: ModuleNotFoundError, VersionNotFoundError, EnvironmentNotFoundError
|
||||
- NOVA_* env var prefix (D-032): NOVA_AWS_* -> AWS_* copy in run_platform.sh
|
||||
- no CLI package (D-027): scripts invoked via shell
|
||||
territory:
|
||||
- "core/**"
|
||||
- "adapters/terraform/adapter.py"
|
||||
- "adapters/terraform/__init__.py"
|
||||
- "schemas/contract.schema.json"
|
||||
- "contracts/**"
|
||||
- "scripts/**"
|
||||
- "tests/**"
|
||||
- "**/*.py"
|
||||
- name: frontend-engineer
|
||||
domain: frontend
|
||||
active: false
|
||||
frameworks:
|
||||
- react
|
||||
- next.js
|
||||
constraints:
|
||||
- component-first
|
||||
- server-components
|
||||
- minimal-client-js
|
||||
territory:
|
||||
- "**/components/**"
|
||||
- "**/pages/**"
|
||||
- "**/hooks/**"
|
||||
- "**/styles/**"
|
||||
- "**/*.tsx"
|
||||
- "**/*.css"
|
||||
- "**/*.vue"
|
||||
reason: Nova Platform has no frontend. Deactivated (D-006).
|
||||
phase_personas: []
|
||||
---
|
||||
|
||||
# Personas — Nova Platform v1.0
|
||||
|
||||
> Active personas for all execution phases. The 3 active personas cover
|
||||
> the full v1.0 scope (contract surface + resolution + engine adapter +
|
||||
> module catalog + Terraform bootstrap + shell reproducibility + tests +
|
||||
> docs). No phase-specific personas are needed.
|
||||
|
||||
## Roster
|
||||
|
||||
| Persona | Domain | Active | Lead coverage |
|
||||
|---------|--------|--------|---------------|
|
||||
| lead-developer | coordination | yes | cross-cutting: `.ciagent/`, root config, architecture docs |
|
||||
| data-engineer | data (terraform/modules) | yes | `modules/`, `terraform/`, stack/environment schemas, module docs |
|
||||
| backend-engineer | backend (python) | yes | `core/`, `adapters/terraform/*.py`, contract schema, `contracts/`, `scripts/`, `tests/` |
|
||||
| frontend-engineer | frontend | NO (D-006) | — |
|
||||
|
||||
## Framework alignment
|
||||
|
||||
The project has NO JavaScript/frontend runtime. Frameworks overridden
|
||||
from config.json defaults to match actual project deps:
|
||||
|
||||
- **lead-developer:** python, bash, terraform, jsonschema, pytest, boto3.
|
||||
- **data-engineer:** terraform, hcl, jsonschema, json (was `["terraform"]` — expanded; dropped nothing).
|
||||
- **backend-engineer:** python, pyyaml, jsonschema, boto3, pytest, moto, bash (was `["python", "fastapi"]` — **dropped `fastapi`** (no HTTP API; shell-invoked); added actual deps).
|
||||
- **frontend-engineer:** unchanged (deactivated).
|
||||
|
||||
## Territory alignment
|
||||
|
||||
Territory globs overridden to match actual file structure. The
|
||||
reference's `nova/`, `pipelines/`, `metrics/`, `.github/workflows/`,
|
||||
`mcp/`, `skills/`, `workflows-src/`, `platform/` dirs do NOT exist.
|
||||
|
||||
- **lead-developer:** `.ciagent/**`, `README.md`, `pyproject.toml`, `requirements-test.txt`, `.gitignore`, `docs/architecture.md`, `docs/consumer-guide.md`.
|
||||
- **data-engineer:** `modules/**`, `terraform/**`, `schemas/stack.schema.json`, `schemas/environment.schema.json`, `docs/modules/**`, `docs/environments/**`, `**/*.tf`, `**/*.tf.json`. NOTE: `schemas/contract.schema.json` is backend-engineer territory.
|
||||
- **backend-engineer:** `core/**`, `adapters/terraform/adapter.py`, `adapters/terraform/__init__.py`, `schemas/contract.schema.json`, `contracts/**`, `scripts/**`, `tests/**`, `**/*.py`. NOTE: `adapters/terraform/policy/` is OOS.
|
||||
- **frontend-engineer:** unchanged (deactivated).
|
||||
|
||||
## Constraint alignment
|
||||
|
||||
### Shared constraints (all active personas)
|
||||
- **engine-agnostic core:** no `aws_*` / `terraform` / `module "` / `provider "` / `resource "` strings outside `adapters/terraform/` (verified by `tests/test_engine_boundary.py`).
|
||||
- **acdl structural conventions preserved with 9 locked deviations:** directory names, file roles, module interface shape, registry format, banner strings, state key convention, tag convention. Deviations locked in CLARIFY.md (D-012, D-013, D-015, D-017, D-018, D-019, D-022, D-023, D-025, D-027).
|
||||
- **no security/identity/CI-workflow:** kyverno, Wiz, Checkov, PolicyEngine, confidence_signal, outbox_writer, ABAC, PAT, JWS, KMS signing, SoD, HITL, attestation, submission_readiness, env_transition, decommission, mode_resolver, onboarding flow beyond bootstrap, metrics, pipelines, `.github/workflows/` — ALL out of scope. Do NOT implement.
|
||||
|
||||
### Persona-specific constraints
|
||||
- **lead-developer:** pragmatic, battle-tested defaults, engine-agnostic core, acdl structural conventions, no security/identity/CI-workflow, local shell reproducibility over CI.
|
||||
- **data-engineer:** schema-first, type-safe, migration-driven, engine terms only in `adapters/terraform/` + `modules/*/terraform/` + `terraform/`, module interface shape, registry entry shape, L2 opaque per D-012, state key `spike/<stack_name>/<environment>/terraform.tfstate`, tag convention.
|
||||
- **backend-engineer:** api-first (contract schema is the API), strict-typing, dependency-injection, `resolve()` pure (D-011), `adapt()` stateless assembler, named exceptions, `NOVA_*` env prefix (D-032), no CLI package (D-027).
|
||||
|
||||
## Phase-specific personas
|
||||
|
||||
None. The 3 active personas cover all 6 execution phases:
|
||||
- Phase 1 (Contract Surface + Schemas + Resolver): backend-engineer.
|
||||
- Phase 2 (Terraform Adapter + Engine Boundary): backend-engineer.
|
||||
- Phase 3 (L1 Primitives + Registry): data-engineer (registry + interfaces + terraform), backend-engineer (registry-loading code).
|
||||
- Phase 4 (L2 Patterns + Terraform Bootstrap + Platform): data-engineer (L2 terraform, bootstrap terraform, platform/ci-vpc/microservice/onboarding terraform), backend-engineer (bootstrap python scripts).
|
||||
- Phase 5 (Shell Reproducibility + Test Suite + Docs): backend-engineer (scripts, tests), lead-developer (README, docs, pyproject), data-engineer (docs/modules, docs/environments).
|
||||
- Phase 6 (Final Review + Ship): lead-developer.
|
||||
@@ -0,0 +1,410 @@
|
||||
# PLAN — Nova Platform v1.0
|
||||
|
||||
> Task-level, wave-ordered, persona-assigned plan for the 5 execution
|
||||
> phases (P1..P5) + the final phase (P6). Consumed by the execute
|
||||
> workflow. Derived from ROADMAP.md phase breakdown + REQUIREMENTS.md
|
||||
> REQ coverage + PERSONAS.md assignments + CLARIFY.md D-011..D-035 +
|
||||
> RESEARCH.md reference shapes.
|
||||
|
||||
## User-Facing Surface
|
||||
|
||||
The platform's user-facing surfaces are **entirely offline-runnable** —
|
||||
no CI required, no AWS credentials required for the primary verification
|
||||
path:
|
||||
|
||||
1. **`scripts/run_platform.sh --check-only`** — Offline validation
|
||||
entrypoint. `bash scripts/run_platform.sh --check-only
|
||||
contracts/static-assets.yml` → exit 0 + `=== PLATFORM CHECK OK ===`
|
||||
iff contract validates → resolves → stack is schema-valid → adapter
|
||||
compiles to structurally-valid HCL. No AWS calls.
|
||||
2. **`scripts/run_platform.sh`** (default mode) — Full apply path.
|
||||
Loads `NOVA_AWS_*` → `AWS_*`, runs terraform init/validate/plan/apply,
|
||||
prints `=== PLATFORM APPLY OK ===`. `--plan-only` stops before apply.
|
||||
3. **`scripts/run_ci.sh`** — Local CI mirror. lint (py_compile) → test
|
||||
(pytest) → check-only. Prints `=== CI PIPELINE OK ===`.
|
||||
4. **`docs/consumer-guide.md`** — Consumer happy-path walkthrough.
|
||||
5. **`README.md`** quickstart — operator/dev quickstart.
|
||||
6. **`.feature`-equivalent scenarios** — `tests/test_run_platform_check_only.py`
|
||||
+ `tests/test_run_ci.py` encode the happy path as executable pytest cases.
|
||||
|
||||
## Happy Path
|
||||
|
||||
Two end-to-end scenarios, written BEFORE execute, verified by automated
|
||||
tests in P5:
|
||||
|
||||
### Scenario A — Offline contract validation (primary gate)
|
||||
|
||||
```bash
|
||||
bash scripts/run_platform.sh --check-only contracts/static-assets.yml
|
||||
# expected: === PLATFORM CHECK OK === ; exit 0
|
||||
```
|
||||
|
||||
Steps: load `core/environments/dev.json` → parse contract → validate
|
||||
against contract schema → resolve to stack (interpolation) → adapt to
|
||||
HCL → validate output structure → print banner → exit 0. No AWS SDK
|
||||
calls, no terraform binary, no network.
|
||||
|
||||
### Scenario B — Local CI mirror
|
||||
|
||||
```bash
|
||||
bash scripts/run_ci.sh
|
||||
# expected: === CI PIPELINE OK === ; exit 0
|
||||
```
|
||||
|
||||
Steps: (1) lint `py_compile $(find core/ adapters/ scripts/ -name '*.py')`;
|
||||
(2) test `pytest`; (3) check-only `run_platform.sh --check-only`. Print
|
||||
banner → exit 0.
|
||||
|
||||
## UX Acceptance Criteria
|
||||
|
||||
v1.0 is accepted iff ALL hold:
|
||||
|
||||
1. **AC-1 (offline validation works):** `run_platform.sh --check-only
|
||||
contracts/static-assets.yml` exits 0 + stdout contains
|
||||
`=== PLATFORM CHECK OK ===`. (tests/test_run_platform_check_only.py)
|
||||
2. **AC-2 (local CI works):** `run_ci.sh` exits 0 + stdout contains
|
||||
`=== CI PIPELINE OK ===`. (tests/test_run_ci.py)
|
||||
3. **AC-3 (contract schema is the API):** all 10 sample contracts
|
||||
validate against contract.schema.json. (tests/test_contract_schema.py)
|
||||
4. **AC-4 (resolver pure + correct):** `resolve()` returns stack
|
||||
validating against stack.schema.json; raises `ModuleNotFoundError`/
|
||||
`VersionNotFoundError`. (tests/test_contract_resolver.py +
|
||||
test_stack_schema.py)
|
||||
5. **AC-5 (adapter compiles + boundary holds):** `adapt(stack, repo_root)` (C-1 fix)
|
||||
emits valid HCL; no `.py` outside `adapters/terraform/` contains
|
||||
forbidden strings. (tests/test_terraform_adapter.py + test_engine_boundary.py)
|
||||
6. **AC-6 (module catalog complete):** registry.json has 13 L1 + 2 L2 =
|
||||
15 entries; all interface.json + terraform/main.tf exist; L2 entries
|
||||
include `terraform_dir` (D-013).
|
||||
7. **AC-7 (Terraform roots + bootstrap exist):** terraform/{bootstrap,
|
||||
ci-vpc,platform,microservice,onboarding}/ per REQ-14..19 + D-022..D-025.
|
||||
Lock table `nova-tfstate-locks`; account parameterized.
|
||||
8. **AC-8 (docs cover consumer journey):** README + docs/{architecture,
|
||||
consumer-guide,modules/index,environments/index,contracts/index}.md
|
||||
exist, no OOS sections. Mechanically checked by grep for OOS section
|
||||
headings ("Security", "Compliance", "OIDC", "Attestation", "ABAC")
|
||||
inside `docs/*.md` + `modules/*/README.md` — zero matches (C-3 fix).
|
||||
9. **AC-9 (reproducibility):** requirements-test.txt pins 5 deps;
|
||||
pyproject.toml no `[project.scripts]`; rotate_spike_key.sh writes
|
||||
.env.secrets (0600); .gitignore covers .env*/terraform state.
|
||||
10. **AC-10 (engine-agnostic invariant):** engine-boundary test passes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Contract Surface + Schemas + Resolver
|
||||
|
||||
**Goal:** A contract can be validated, resolved to a stack, environment
|
||||
checked, stack validated — all offline, no apply, no engine terms in
|
||||
contract/core layer.
|
||||
|
||||
**REQs:** REQ-01, REQ-02, REQ-03, REQ-04, REQ-05, REQ-06, REQ-23,
|
||||
REQ-24, REQ-27, REQ-28.
|
||||
|
||||
**Personas:** backend-engineer (contract schema, resolver,
|
||||
environment_check, contracts, tests); data-engineer (stack schema,
|
||||
environment schema, dev.json).
|
||||
|
||||
**Ships as:** `v0.1.1` on `phase/01-contract-surface-schemas-resolver`.
|
||||
|
||||
### Wave 1 (parallel — schemas + env data)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P1-W1-T1 | backend | REQ-01, D-015 | `schemas/contract.schema.json` | Draft 2020-12; `required:[id,name,environment,infrastructure]`; `id` pattern `^[a-z][a-z0-9-]{2,5}$`; `infrastructure` ARRAY (D-015) items `{module,version?,inputs}`; no engine terms. |
|
||||
| P1-W1-T2 | data | REQ-04, D-012 | `schemas/stack.schema.json` | Flat per D-012; `required:[contract_id,contract_name,environment,resources]`; resources `{module,version,source,inputs}`; no stack wrapper/relationships/nfrs. |
|
||||
| P1-W1-T3 | data | D-017, D-018 | `schemas/environment.schema.json` | `required:[name,account_id,region,state_backend,network]`; no runner_role_arn/autonomy/confidence_threshold; `additionalProperties:false`. |
|
||||
| P1-W1-T4 | data | REQ-06, D-018 | `core/environments/dev.json` | Validates against environment schema; `name:"dev"`, placeholder account, `us-east-1`, state_backend, network. |
|
||||
|
||||
### Wave 2 (resolver + env_check — depends on W1)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P1-W2-T1 | backend | REQ-03, D-011, D-016, D-037 | `core/contract_resolver.py` | `resolve(contract, registry, modules_dir) -> dict` (D-011). Interpolation kept (D-016). Named exceptions `ModuleNotFoundError`/`VersionNotFoundError`. L2 opaque (D-012). **No `source` in stack (D-037/C-1 fix) — stack is engine-agnostic `{contract_id, contract_name, environment, resources:[{module,version,inputs}]}`.** No engine terms. |
|
||||
| P1-W2-T2 | backend | REQ-05, D-019 | `core/environment_check.py` | `check(env_name, environments_dir) -> dict` (D-019). Raises `EnvironmentNotFoundError`. No tuple/onboarding_message/CLI. No engine terms. |
|
||||
|
||||
### Wave 3 (sample contracts — depends on W1+W2)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P1-W3-T1 | backend | REQ-02, D-033, D-035 | `contracts/static-assets.{yaml,dev.yml,qa.yml,prod.yml,dr.yml}` | Validate against contract schema; `static-assets` L2; inputs incl `bucket_name`/`index_document` (D-035); per-env differ only in `environment` (D-033); interpolation tokens. |
|
||||
| P1-W3-T2 | backend | REQ-02, D-033 | `contracts/microservice.{yaml,dev.yml,qa.yml,prod.yml,dr.yml}` | Validate; `microservice` L2; per-env differ only in `environment`; interpolation. |
|
||||
|
||||
### Wave 4 (parallel — tests, depends on W1-W3)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P1-W4-T1 | backend | REQ-23 | `tests/test_contract_resolver.py` | Happy path, `ModuleNotFoundError`, `VersionNotFoundError`, empty infra. Pass. |
|
||||
| P1-W4-T2 | backend | REQ-24 | `tests/test_environment_check.py` | `check("dev",...)` returns dict; missing → `EnvironmentNotFoundError`; malformed. Pass. |
|
||||
| P1-W4-T3 | backend | REQ-27 | `tests/test_contract_schema.py` | All 10 contracts validate; negative cases raise. Pass. |
|
||||
| P1-W4-T4 | backend | REQ-28 | `tests/test_stack_schema.py` | `resolve()` stack validates; negative cases. Pass. |
|
||||
|
||||
### Must-haves
|
||||
- 3 schemas parse as valid JSON Schema draft 2020-12.
|
||||
- `resolve()` + `check()` exposed with named exceptions; no engine terms.
|
||||
- `dev.json` + 10 contracts validate.
|
||||
- `pytest -q tests/test_contract_resolver.py tests/test_environment_check.py tests/test_contract_schema.py tests/test_stack_schema.py` exit 0.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Terraform Adapter + Engine Boundary
|
||||
|
||||
**Goal:** A resolved stack compiles to valid HCL via the stateless
|
||||
adapter; engine boundary enforced by grep test.
|
||||
|
||||
**REQs:** REQ-07, REQ-08, REQ-09, REQ-25, REQ-26.
|
||||
|
||||
**Personas:** backend-engineer (adapter + tests).
|
||||
|
||||
**Ships as:** `v0.1.2` on `phase/02-terraform-adapter-engine-boundary`.
|
||||
|
||||
> **Dependency (C-4 fix):** P2 depends on P3-W1 (registry.json). The
|
||||
> adapter loads `registry.json` internally (C-1 fix). Reorder: run
|
||||
> P3-W1 (registry.json only) before P2-W1, then continue P3-W2 (L1
|
||||
> terraform dirs) in parallel with P2.
|
||||
|
||||
### Wave 1 (adapter — depends on P3-W1 registry.json)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P2-W1-T1 | backend | REQ-07, REQ-08, D-013, D-037 | `adapters/terraform/adapter.py`, `adapters/terraform/__init__.py` | `adapt(stack, repo_root) -> str` (C-1/D-037 fix). Loads `modules/registry.json` internally to map `module` → `terraform_dir`. Stateless, <250 lines. Emits `module "x" { source; <inputs> }` per resource. L2 `terraform_dir` from registry (D-013). `__init__.py` re-exports. ONLY place engine terms appear. |
|
||||
|
||||
### Wave 2 (parallel — tests)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P2-W2-T1 | backend | REQ-25 | `tests/test_terraform_adapter.py` | Single/multi resource, input passthrough, HCL validity. Pass. |
|
||||
| P2-W2-T2 | backend | REQ-09, REQ-26, D-034 | `tests/test_engine_boundary.py` | Grep `.py` in core/schemas/contracts/tests/scripts/root; exclude adapters/terraform/ + modules/ + .tf/.md/.json; zero matches for forbidden strings. Pass. |
|
||||
|
||||
### Must-haves
|
||||
- `adapt()` stateless, <250 lines, returns HCL.
|
||||
- `__init__.py` re-exports.
|
||||
- Both tests pass; boundary proven.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — L1 Primitives + Registry
|
||||
|
||||
**Goal:** Full module catalog — registry.json indexing 13 L1 + 2 L2,
|
||||
each L1 with interface.json + terraform/ (main/variables/outputs/versions/locals).
|
||||
|
||||
**REQs:** REQ-10, REQ-11, REQ-13.
|
||||
|
||||
**Personas:** data-engineer (registry, 13 L1 interface.json + terraform,
|
||||
READMEs, docs/modules/index); backend-engineer (registry test, conftest).
|
||||
|
||||
**Ships as:** `v0.1.3` on `phase/03-l1-primitives-registry`.
|
||||
|
||||
### Wave 1 (parallel — registry + 13 L1 interface.json + READMEs + docs index)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P3-W1-T1 | data | REQ-10, D-013 | `modules/registry.json` | 15 entries; L1 `{interface,terraform_dir,published_at,deprecated,kind:"l1"}`; L2 includes `terraform_dir` (D-013). |
|
||||
| P3-W1-T2..T14 | data | REQ-11, D-014 | `modules/l1/{s3,vpc,ecs-cluster,ecs-service,iam-role,alb,ecr,cloudfront,waf,rds,kms-key,dynamodb,uptime}/interface.json` | `{name,version,kind:"l1",type,description,inputs,outputs}` + `resources[]` for multi-resource. No `nfrs`/`intra_refs` (D-014). s3 stays ref interface (D-035). |
|
||||
| P3-W1-T15 | data | REQ-13, D-029 | `modules/README.md`, `modules/README-TEMPLATE.md` | L1/L2 distinction, registry format, add-a-module. 13+2 tables. Trimmed of security/compliance. DROP NFRs + Compliance sections. No STANDARDS.md. |
|
||||
| P3-W1-T16 | data | REQ-34, D-028 | `docs/modules/index.md` | Catalog table → links to `modules/l1/<name>/README.md` + `modules/l2/<name>/README.md`. 15 rows. |
|
||||
|
||||
### Wave 2 (parallel — 13 L1 terraform/ dirs)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P3-W2-T1..T13 | data | REQ-11 | `modules/l1/<name>/terraform/{main,variables,outputs,versions,locals}.tf` | `count = var.enabled ? 1 : 0`; `required_version = ">= 1.9, < 1.10"`; `aws ~> 5.0`; variables match interface.json inputs. |
|
||||
|
||||
### Wave 3 (registry test + conftest)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P3-W3-T1 | backend | REQ-10 | `tests/test_registry.py` | 15 entries, L2 has `terraform_dir`, interface paths resolve, terraform_dir/main.tf exist. Pass. |
|
||||
| P3-W3-T2 | backend | (support) | `tests/conftest.py` (partial) | `repo_root` + `registry` fixtures. No `stack_instance`/`policy_check_result_schema`. |
|
||||
|
||||
### Must-haves
|
||||
- registry.json valid, 15 entries, L2 has `terraform_dir`.
|
||||
- 13 L1 interface.json + terraform dirs exist, conform to D-014.
|
||||
- READMEs trimmed; docs/modules/index links to all 15.
|
||||
- test_registry.py + conftest fixtures pass.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — L2 Patterns + Terraform Bootstrap + Platform
|
||||
|
||||
**Goal:** Two L2 patterns composing L1 internally; AWS bootstrap
|
||||
scripted; platform/ci-vpc/microservice/onboarding roots exist.
|
||||
|
||||
**REQs:** REQ-12, REQ-14, REQ-15, REQ-16, REQ-17, REQ-18, REQ-19.
|
||||
|
||||
**Personas:** data-engineer (L2 interfaces + terraform, bootstrap
|
||||
policy + README, 4 terraform roots); backend-engineer (bootstrap py
|
||||
scripts, rotate_spike_key.sh).
|
||||
|
||||
**Ships as:** `v0.1.4` on `phase/04-l2-patterns-bootstrap-platform`.
|
||||
|
||||
### Wave 1 (parallel — L2 interfaces + bootstrap policy + 4 roots + README)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P4-W1-T1 | data | REQ-12, D-012, D-013 | `modules/l2/microservice/interface.json` | `{name,version,kind:"l2",description,inputs,outputs}` — L2-level only, no children/wires. |
|
||||
| P4-W1-T2 | data | REQ-12, D-012, D-035 | `modules/l2/static-assets/interface.json` | Inputs incl `bucket_name`/`index_document` (D-035). |
|
||||
| P4-W1-T3 | data | REQ-15, D-026 | `terraform/bootstrap/spike_runner_policy.json` | Account parameterized (NOT hardcoded). Grants S3/DynamoDB lock/ECS/ECR/ELB/IAM/EC2/CloudFront/WAF/KMS. DROP Lambda/Secrets/SNS/CostExplorer/OIDC. |
|
||||
| P4-W1-T4 | data | REQ-16, D-024 | `terraform/ci-vpc/main.tf` | Short-lived test VPC; VPC+2 subnets+IGW+route table+ECS SG+cluster; 4 outputs; state key `spike/ci-vpc/terraform.tfstate`. |
|
||||
| P4-W1-T5 | data | REQ-17, D-023 | `terraform/platform/main.tf` | ONLY shared VPC per D-023; VPC+2 subnets+IGW+route table+ECS SG; outputs `vpc_id`/`subnet_ids`/`ecs_security_group_id`. DROP Lambda/DynamoDB/KMS/Secrets/SNS. |
|
||||
| P4-W1-T6 | data | REQ-18 | `terraform/microservice/main.tf` | Instantiates L2 module + `data.terraform_remote_state` to platform VPC. State key `spike/microservice/<env>/terraform.tfstate`. |
|
||||
| P4-W1-T7 | data | REQ-19, D-025 | `terraform/onboarding/main.tf` | IAM ROLE (not user) per D-025. Cross-account `sts:AssumeRole`. NO OIDC. `consumer_repo`/`owner_id` vars. DROP `lambda:InvokeFunctionUrl`. Outputs role arn/name. |
|
||||
| P4-W1-T8 | data | REQ-14, D-022 | `terraform/bootstrap/README.md` | Documents `nova-tfstate-locks` (D-022 — NOT `nova-outbox`), `nova-spike-runner`, parameterized account, `NOVA_BOOTSTRAP_AWS_*`. |
|
||||
|
||||
### Wave 2 (parallel — 2 L2 terraform composing L1, depends on W1 + P3)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P4-W2-T1 | data | REQ-12, D-012, D-038 | `modules/l2/microservice/terraform/{main,variables,outputs,versions}.tf` | `module "vpc"{source="../../l1/vpc/terraform"}` + cluster/service/role/ecr/alb (6 L1s per D-038). Variables match L2 interface. |
|
||||
| P4-W2-T2 | data | REQ-12, D-012, D-035, D-038 | `modules/l2/static-assets/terraform/{main,variables,outputs,versions}.tf` | `module "s3"{source="../../l1/s3/terraform"}` + cloudfront + kms (3 L1s per D-038, drops waf). `index_document` → s3 website. |
|
||||
|
||||
### Wave 3 (parallel — bootstrap py scripts + rotate, depends on W1 policy)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P4-W3-T1 | backend | REQ-14, D-022 | `terraform/bootstrap/create_state_backend.py` | S3 `nova-tfstate-<account>-<region>` + DynamoDB `nova-tfstate-locks`. Idempotent. `NOVA_BOOTSTRAP_AWS_*`→`NOVA_AWS_*`→`AWS_*`. `py_compile` clean. No engine-boundary violation (boto3, not HCL). |
|
||||
| P4-W3-T2 | backend | REQ-15, D-026 | `terraform/bootstrap/create_iam_user.py` | `nova-spike-runner` + policy + key. Prints `NOVA_AWS_*`. Idempotent. `py_compile` clean. No HCL strings. |
|
||||
| P4-W3-T3 | backend | REQ-22, D-032 | `scripts/rotate_spike_key.sh` | Rotates key → `.env.secrets` (0600). `NOVA_AWS_*` (D-032). `bash -n` clean. |
|
||||
|
||||
### Must-haves
|
||||
- Both L2 interface.json + terraform exist; compose L1 via `module` blocks with `../../l1/...`.
|
||||
- bootstrap/ has 4 files; policy account-parameterized; lock table `nova-tfstate-locks`.
|
||||
- 4 terraform roots exist per D-023/D-024/D-025.
|
||||
- rotate_spike_key.sh syntax-valid.
|
||||
- Bootstrap py `py_compile` clean, no engine-boundary violation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Shell Reproducibility + Test Suite + Docs
|
||||
|
||||
**Goal:** Platform fully reproducible from shell — `run_platform.sh
|
||||
--check-only` + `run_ci.sh` exit 0 with banners; full test suite
|
||||
passes; docs complete. Happy path green.
|
||||
|
||||
**REQs:** REQ-20, REQ-21, REQ-22 (verify), REQ-29, REQ-30, REQ-31,
|
||||
REQ-32, REQ-33, REQ-34, REQ-35, REQ-36, REQ-37, REQ-38.
|
||||
|
||||
**Personas:** backend-engineer (shell scripts, shell tests, conftest
|
||||
final, pyproject, requirements); lead-developer (README, architecture,
|
||||
consumer-guide); data-engineer (docs/modules/index finalize,
|
||||
environments/index, contracts/index).
|
||||
|
||||
**Ships as:** `v0.1.5` on `phase/05-shell-reproducibility-tests-docs`.
|
||||
|
||||
### Wave 1 (parallel — scripts + deps + docs)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P5-W1-T1 | backend | REQ-20, D-020, D-031, D-032 | `scripts/run_platform.sh` | Flags: `--check-only`/`--plan-only`/`--quiet`/`--help`. check-only: env_check→validate→resolve→adapter→validate output→`=== PLATFORM CHECK OK ===`. plan-only: +creds (`NOVA_AWS_*`→`AWS_*` then unset)→init/validate/plan→`=== PLATFORM PLAN OK ===`. default: +apply→`=== PLATFORM APPLY OK ===`. `bash -n` clean. check-only exits 0 offline. |
|
||||
| P5-W1-T2 | backend | REQ-21, D-021 | `scripts/run_ci.sh` | 3 stages: lint (glob py_compile)→test (pytest)→check-only. `=== CI PIPELINE OK ===`. `--quiet`. `bash -n` clean. |
|
||||
| P5-W1-T3 | backend | REQ-31, D-027 | `pyproject.toml`, `requirements-test.txt` | No `[project.scripts]`, no `nova/` dir. 5 test deps. `addopts="-v --tb=short"`. `markers=[offline,slow]`. packages.find: `core,core.*,adapters.*`. |
|
||||
| P5-W1-T4 | backend | REQ-38 | `.gitignore` | Verify/extend: `.env*`, terraform state, credentials, `__pycache__/`, `.ciagent/logs/`, `nova_platform.egg-info/`. |
|
||||
| P5-W1-T5 | lead | REQ-32 | `README.md` | What platform is, run offline, run tests, run against AWS, repo layout, credentials (static-key only), consumer-guide pointer. No security/identity sections. |
|
||||
| P5-W1-T6 | lead | REQ-33 | `docs/architecture.md` | Mirrors `.ciagent/ARCHITECTURE.md`. 4 layers + boundary + OOS list + catalog. No cross-cutting sections. |
|
||||
| P5-W1-T7 | lead | REQ-37 | `docs/consumer-guide.md` | Infra-only: create repo, write contract, run check-only, run against AWS. Interpolation table. DROP OIDC/reusable-workflow/decommission/compliance. |
|
||||
| P5-W1-T8 | data | REQ-34, D-028 | `docs/modules/index.md` (finalize) | Catalog table → 15 module READMEs. |
|
||||
| P5-W1-T9 | data | REQ-36, D-018 | `docs/environments/index.md` | Env model: account/network/state backend — NO IAM/ABAC. Autonomy table: dev autonomous; qa/prod/dr manual. No HITL gates. |
|
||||
| P5-W1-T10 | data | REQ-35, D-033 | `docs/contracts/index.md` | Array-based infrastructure schema (D-015) + samples + per-env variants (D-033). Interpolation table. |
|
||||
|
||||
### Wave 2 (parallel — shell tests + conftest, depends on W1)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P5-W2-T1 | backend | REQ-29 | `tests/test_run_platform_check_only.py` | `subprocess` `run_platform.sh --check-only contracts/static-assets.yml`; assert exit 0 + `=== PLATFORM CHECK OK ===`. Pass. |
|
||||
| P5-W2-T2 | backend | REQ-30 | `tests/test_run_ci.py` | `subprocess` `run_ci.sh`; assert exit 0 + `=== CI PIPELINE OK ===`. Pass. |
|
||||
| P5-W2-T3 | backend | (support) | `tests/conftest.py` (finalize) | `repo_root`, `registry`, `stack_schema`, `contract_schema` fixtures. `sys.path.insert` for core/ + adapters/. |
|
||||
|
||||
### Wave 3 (full-suite green — depends on W1-W2)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P5-W3-T1 | backend | (gate) | none | `pytest -q` all pass. `bash scripts/run_ci.sh` exit 0 + banner. |
|
||||
|
||||
### Must-haves (MVP/UX gate)
|
||||
- `run_platform.sh --check-only contracts/static-assets.yml` exit 0 + `=== PLATFORM CHECK OK ===`.
|
||||
- `run_ci.sh` exit 0 + `=== CI PIPELINE OK ===`.
|
||||
- Full `pytest` suite passes.
|
||||
- pyproject + requirements configure pytest/py_compile, no CLI.
|
||||
- .gitignore covers all patterns.
|
||||
- 6 docs exist, no OOS sections.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Final Review + Ship
|
||||
|
||||
**Goal:** Review v1.0 against AC-1..AC-10, audit for boundary leaks +
|
||||
OOS-creep, ship: merge `phase/06`→`milestone/v1.0-nova-platform`→`main`,
|
||||
tag `v1.0.0` (major — initial release per D-001), Gitea release, delete
|
||||
branches.
|
||||
|
||||
**REQs:** none new.
|
||||
|
||||
**Personas:** lead-developer (review, audit, ship); backend/data
|
||||
consulted for fix-forward.
|
||||
|
||||
**Ships as:** `v1.0.0` (major tag).
|
||||
|
||||
### Wave 1 (review)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P6-W1-T1 | lead | AC-1..AC-10 | none | Walk all 10 ACs. Record pass/fail. All must PASS before proceeding. Escalate on failure (supervised). |
|
||||
|
||||
### Wave 2 (fix-forward, conditional)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P6-W2-T1..Tn | backend/data | (varies) | (varies) | Fix specific AC failures. Re-verify. Max 2 revision iterations. |
|
||||
|
||||
### Wave 3 (audit)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P6-W3-T1 | lead | REQ-09, OOS list | none | (1) `pytest -q tests/test_engine_boundary.py` pass. (2) Grep repo for OOS file names (policy_engine, confidence_signal, outbox_writer, abac_*, pat_*, jws_*, kms_signing, hitl_*, attestation_*, separation_*, submission_*, env_transition, decommission_*, mode_resolver, onboarding.py, regression_verify*, metrics/, pipelines/, .github/workflows/, adapters/kyverno-json, adapters/wiz, adapters/checkov, schemas/pipeline*, schemas/deploy-pipeline*, schemas/policy_check_result*, schemas/metrics_*). ZERO matches. (3) No STANDARDS.md, no PPTX/marp. |
|
||||
|
||||
### Wave 4 (ship)
|
||||
|
||||
| Task | Persona | REQs | Files | Must-have |
|
||||
|------|---------|------|-------|-----------|
|
||||
| P6-W4-T1 | lead | D-001, D-009 | git refs | Merge `phase/06-final-review-ship`→`milestone/v1.0-nova-platform`. Merge milestone→`main`. Tag `v1.0.0` on main. **`confirm_before_ship=true` per D-009 — escalate before tagging.** Gitea release via `NOVA_FORGE_TOKEN`. Delete phase/0*+1* branches. Verify tag + release URL. |
|
||||
|
||||
### Must-haves
|
||||
- All 10 ACs PASS.
|
||||
- Engine boundary passes; zero OOS files.
|
||||
- `v1.0.0` tag on main.
|
||||
- Gitea release `v1.0.0` created.
|
||||
- All phase branches deleted.
|
||||
|
||||
---
|
||||
|
||||
## Cross-phase invariants (hold after EVERY phase)
|
||||
|
||||
1. **Engine boundary:** no `.py` outside `adapters/terraform/` contains
|
||||
`aws_`/`module "`/`terraform`/`provider "`/`resource "` (REQ-09/D-034).
|
||||
2. **No OOS-creep:** no file from PROJECT.md/REQUIREMENTS.md OOS list
|
||||
created in any phase.
|
||||
3. **Structural conventions:** directory names, file roles, interface
|
||||
shape, registry shape, banner strings, state key, tag convention
|
||||
preserved without deviation.
|
||||
4. **Tests stay green:** once a test file exists, subsequent phases must
|
||||
not break it. `pytest -q` passes at end of every phase.
|
||||
5. **Supervised escalation:** `ship` (P6-W4-T1) + verification failures
|
||||
escalate to human per `escalation_timeout_ms=300000`.
|
||||
|
||||
## Wave dependency graph
|
||||
|
||||
```
|
||||
P1: W1(schemas+env) → W2(resolver+env_check) → W3(contracts) → W4(tests)
|
||||
P3-W1(registry.json ONLY) → P2: W1(adapter loads registry) → W2(adapter tests + boundary) [C-4 fix: P3-W1 before P2-W1]
|
||||
P3: W1(registry+13 L1 interface+READMEs+docs) → W2(13 L1 terraform) → W3(registry test+conftest) [W1 split: registry.json first, then rest]
|
||||
P4: W1(L2 interfaces+bootstrap policy+4 roots+README) → W2(2 L2 terraform per D-038) → W3(2 bootstrap py+rotate) [deps P3 L1 terraform]
|
||||
P5: W1(2 scripts+pyproject+reqs+gitignore+README+arch+consumer-guide+3 docs) → W2(2 shell tests+conftest) → W3(full-suite green) [deps P1-P4]
|
||||
P6: W1(review AC-1..10) → [W2 fix-forward] → W3(audit + docs OOS grep C-3) → W4(ship v1.0.0) [deps P5]
|
||||
```
|
||||
|
||||
> **Concurrency note (C-5):** P3-W1 (13 L1 interface.json tasks) +
|
||||
> P3-W2 (13 L1 terraform tasks) + P4-W1 (8 tasks) exceed
|
||||
> `max_concurrent_agents=5`. These waves batch-serialize into 3-5
|
||||
> rounds. Schedule estimate reflects ~2-3× wall-clock for P3.
|
||||
|
||||
Total: 6 phases, ~45 tasks across ~13 waves, 3 active personas (max
|
||||
concurrency 5). No wave has >5 parallel tasks.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Nova Platform — Infrastructure Delivery
|
||||
|
||||
> **Derived from** the Nova reference (`acdl`) — a simplified,
|
||||
> infrastructure-only platform. The DevSecOps, security-scoring,
|
||||
> identity/ABAC, audit-ledger, and central CI-pipeline-contract machinery
|
||||
> of the reference are intentionally **removed**. What remains is the
|
||||
> infrastructure-delivery core: a consumer declares intent via a YAML
|
||||
> contract; the platform resolves it to a stack, compiles it through the
|
||||
> Terraform adapter, and applies it. Structural conventions (directory
|
||||
> names, file roles, module interface shape, registry format) are
|
||||
> preserved with 9 locked deviations (D-012, D-013, D-015, D-017,
|
||||
> D-018, D-019, D-022, D-023, D-025, D-027 — see CLARIFY.md) from the
|
||||
> reference.
|
||||
|
||||
## Vision / Core Value
|
||||
|
||||
Consumers declare infrastructure intent; the platform delivers it. The
|
||||
platform absorbs one friction: the cognitive load of getting the
|
||||
infrastructure right. A consumer writes a small YAML contract that names
|
||||
one or more modules by name + version, selects an environment, and
|
||||
supplies module-specific inputs. The platform resolves the contract to a
|
||||
stack instance, compiles it through the Terraform adapter, and applies
|
||||
it. There is no security scoring, no audit chain, no identity layer, and
|
||||
no reusable CI workflow — those are explicitly out of scope.
|
||||
|
||||
Source of truth for **how**: `docs/architecture.md` +
|
||||
`.ciagent/ARCHITECTURE.md`. Where the two conflict, ARCHITECTURE.md wins.
|
||||
|
||||
## North Star
|
||||
|
||||
A merged change progresses through lower environments without a platform
|
||||
engineer authoring a workflow, a configuration file, or a Terraform
|
||||
module. A consumer declares infrastructure and the platform applies it.
|
||||
Every deployment is reproducible from the shell, not just in CI.
|
||||
|
||||
## Core Tenets
|
||||
|
||||
1. **Operations are Declared, Not Executed.** Consumers define what
|
||||
they need; the platform reconciles, provisions, and applies.
|
||||
2. **The Delivery Lifecycle is a Sovereign Boundary.** The platform
|
||||
governs infrastructure only; it does not reach into upstream product
|
||||
/ SDLC. Integration is only through the validated contract boundary.
|
||||
3. **Dev is Autonomous; Higher Environments are Manual.** Dev applies
|
||||
autonomously. QA/prod/dr are applied by an operator (no attestation
|
||||
machinery — out of scope).
|
||||
4. **Infrastructure is Consumed, Not Maintained.** No node/OS/bare-metal
|
||||
lifecycle. The platform manages environments (accounts, VPCs, state
|
||||
backends); consumers provide none.
|
||||
5. **One Consumer Surface.** A consumer writes a YAML contract and a
|
||||
thin shell invocation. That is the entire surface.
|
||||
|
||||
## Domain Boundaries
|
||||
|
||||
- **In scope:** environment progression; cloud resource lifecycle;
|
||||
contract resolution; Terraform adapter; module catalog (L1 primitives +
|
||||
L2 patterns); local shell reproducibility; offline tests.
|
||||
- **Out of scope:** application business logic; IDE workflows; product
|
||||
backlog; security scoring; policy enforcement; audit ledger;
|
||||
confidence signals; identity/ABAC; HITL attestation; reusable CI
|
||||
workflows; metrics/telemetry of the platform itself.
|
||||
- **Interface:** upstream systems integrate through the contract
|
||||
boundary (`schemas/contract.schema.json`). The platform validates,
|
||||
resolves, and reconciles the target state.
|
||||
|
||||
## Scope: Nova Platform is Downstream of PDLC
|
||||
|
||||
The Product Development Lifecycle (PDLC) — product backlog, code
|
||||
authorship, IDE workflows, application business logic — is **upstream**
|
||||
of Nova Platform. Nova Platform never reaches into the PDLC. Its domain
|
||||
is **infrastructure + delivery only**: environment progression, cloud
|
||||
resource lifecycle.
|
||||
|
||||
Integration between the PDLC and Nova Platform is **only** through the
|
||||
validated contract boundary (`schemas/contract.schema.json`).
|
||||
|
||||
## Decisions (locked in init)
|
||||
|
||||
- **D-001:** Milestone type = `major` (first release, no prior tags).
|
||||
The final phase of v1.0 ships `v1.0.0` and that IS the initial release.
|
||||
- **D-002:** Module count for v1.0 = all 13 L1 primitives + 2 L2 patterns
|
||||
in one milestone (matches the reference v1.0 shape).
|
||||
- **D-003:** `config.git.branching_strategy` = `phase` (canonical
|
||||
CIAgent branch hierarchy; fresh project).
|
||||
- **D-004:** `config.git.auto_commit` / `auto_push` = `true` / `true`.
|
||||
- **D-005:** `config.verification.test_first` = `false`.
|
||||
- **D-006:** Personas = lead-developer + data-engineer (terraform) +
|
||||
backend-engineer (core python); frontend-engineer deactivated (no UI).
|
||||
- **D-007:** `config.policy` removed entirely (no policy engine).
|
||||
`config.ideation.categories` reduced to quality/architecture/coverage/
|
||||
improvement (security dropped).
|
||||
- **D-008:** `config.secrets.scopes` = forge/gitea/github/gitlab +
|
||||
openai/anthropic/ollama_cloud (model backends). `NOVA_FORGE_TOKEN` is
|
||||
the gitea scope var.
|
||||
- **D-009:** `config.ship.confirm_before_ship` = `true` (supervised
|
||||
autonomy escalates on ship).
|
||||
- **D-010:** `config.telemetry.persist` = `true` (CIAgent telemetry !=
|
||||
platform metrics; the metrics layer is dropped but CIAgent's own
|
||||
run audit trail is preserved).
|
||||
@@ -0,0 +1,200 @@
|
||||
# Nova Platform — Requirements (v1.0)
|
||||
|
||||
> Inaugural milestone. Builds the simplified infrastructure-delivery
|
||||
> platform derived from the Nova reference (`acdl`). The security/policy
|
||||
> /identity/audit/CI-pipeline machinery of the reference is
|
||||
> intentionally out of scope (see `PROJECT.md` decisions D-007).
|
||||
|
||||
## Decisions (locked in init CLARIFY, supervised autonomy)
|
||||
|
||||
- **D-001:** Milestone type = `major` (first release, no prior tags).
|
||||
The final phase of v1.0 ships tag `v1.0.0` — that IS the initial
|
||||
release. No separate milestone minor tag (major milestone: the final
|
||||
phase's patch line starts the new major).
|
||||
- **D-002:** v1.0 ships all 13 L1 primitives + 2 L2 patterns in one
|
||||
milestone (matches reference v1.0 shape).
|
||||
- **D-003:** `config.git.branching_strategy` = `phase`.
|
||||
- **D-004:** `auto_commit` / `auto_push` = `true` / `true`.
|
||||
- **D-005:** `test_first` = `false`.
|
||||
- **D-006:** Personas = lead-developer + data-engineer + backend-engineer
|
||||
(frontend-engineer deactivated, no UI).
|
||||
- **D-007:** `config.policy` removed; `ideation.categories` reduced
|
||||
(security dropped).
|
||||
- **D-008:** `secrets.scopes` keeps forge + model-backend scopes.
|
||||
- **D-009:** `ship.confirm_before_ship` = `true` (supervised ship gate).
|
||||
- **D-010:** `telemetry.persist` = `true` (CIAgent audit trail only).
|
||||
|
||||
## Category: Contract Surface (feat)
|
||||
|
||||
- **REQ-01:** `schemas/contract.schema.json` (JSON Schema draft
|
||||
2020-12) defines the contract envelope: `id` (string, required),
|
||||
`name` (string, required), `environment` (string, required, one of
|
||||
`dev|qa|prod|dr`), `infrastructure` (array, required, min 1 item) of
|
||||
objects each with `module` (string), `version` (semver string), and
|
||||
`inputs` (object). No `aws_*` or engine terms permitted in the
|
||||
schema. Validated by `tests/test_contract_schema.py`.
|
||||
- **REQ-02:** `contracts/static-assets.yaml` and
|
||||
`contracts/microservice.yaml` are sample consumer contracts that
|
||||
validate against REQ-01. Each has per-environment variants
|
||||
(`*.dev.yml`, `*.qa.yml`, `*.prod.yml`, `*.dr.yml`).
|
||||
|
||||
## Category: Resolution (feat)
|
||||
|
||||
- **REQ-03:** `core/contract_resolver.py` exposes
|
||||
`resolve(contract: dict, registry: dict) -> dict` that takes a
|
||||
validated contract and the module registry and returns a Stack
|
||||
instance conforming to `schemas/stack.schema.json`. Pure function:
|
||||
no I/O, no engine terms. Raises `ModuleNotFoundError` on unknown
|
||||
module, `VersionNotFoundError` on unknown version.
|
||||
- **REQ-04:** `schemas/stack.schema.json` defines the Stack shape:
|
||||
`contract_id`, `contract_name`, `environment`, and `resources`
|
||||
(array of `{module, version, inputs}` — NO `source` field; the
|
||||
adapter loads `registry.json` to resolve `module` → `terraform_dir`
|
||||
per C-1 grill fix). The stack is engine-agnostic: no `source`, no
|
||||
Terraform paths, no `aws_*` terms (engine terms appear only in the
|
||||
adapter + modules/terraform/, NOT in the contract or stack).
|
||||
- **REQ-05:** `core/environment_check.py` exposes
|
||||
`check(env_name: str, environments_dir: Path) -> dict` that loads
|
||||
`core/environments/<env_name>.json` and returns the environment
|
||||
record (account, region, state_backend). Raises
|
||||
`EnvironmentNotFoundError` on missing env.
|
||||
- **REQ-06:** `core/environments/dev.json` is the sample dev
|
||||
environment (offline-friendly: uses local emulators where possible,
|
||||
AWS where required).
|
||||
|
||||
## Category: Engine Adapter (feat)
|
||||
|
||||
- **REQ-07:** `adapters/terraform/adapter.py` exposes
|
||||
`adapt(stack: dict, repo_root: Path) -> str` that takes a Stack and
|
||||
the repo root Path, loads `modules/registry.json` internally to map
|
||||
`module` → `terraform_dir`, and emits Terraform HCL: a
|
||||
`module "x" { source = ...; <inputs> }` block per resource. Stateless
|
||||
assembler — no `terraform` invocation, no state files, no plan files.
|
||||
The ONLY place `aws_*` / Terraform terms appear in code (per C-1
|
||||
grill fix — the adapter loads the registry inside the engine
|
||||
boundary, not the resolver).
|
||||
- **REQ-08:** `adapters/terraform/__init__.py` re-exports `adapt`.
|
||||
The adapter is behind no protocol (single engine; the reference's
|
||||
`PolicyEngine` pattern is out of scope).
|
||||
- **REQ-09:** `tests/test_engine_boundary.py` asserts that no file
|
||||
outside `adapters/terraform/` contains the strings `aws_`, `module "`,
|
||||
`terraform`, `provider "`, or `resource "`. Engine agnosticism is
|
||||
verified by grep, not by convention.
|
||||
|
||||
## Category: Module Catalog (feat)
|
||||
|
||||
- **REQ-10:** `modules/registry.json` indexes every module + version
|
||||
with `{interface, terraform_dir, published_at, deprecated, kind}`.
|
||||
Matches the reference's registry shape exactly.
|
||||
- **REQ-11:** Each L1 primitive has `modules/l1/<name>/interface.json`
|
||||
(inputs/outputs, no engine terms) and
|
||||
`modules/l1/<name>/terraform/main.tf`. Primitives (13): s3, vpc,
|
||||
ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds,
|
||||
kms-key, dynamodb, uptime.
|
||||
- **REQ-12:** Each L2 pattern has `modules/l2/<name>/interface.json`
|
||||
and `modules/l2/<name>/terraform/main.tf` that composes L1
|
||||
primitives via `module` blocks. Patterns (2): microservice,
|
||||
static-assets.
|
||||
- **REQ-13:** `modules/README.md` documents the L1/L2 distinction,
|
||||
the registry format, and how to add a module. Matches the
|
||||
reference's `modules/README.md` shape (minus the security/attestation
|
||||
sections).
|
||||
|
||||
## Category: Terraform Bootstrap + Platform (feat)
|
||||
|
||||
- **REQ-14:** `terraform/bootstrap/create_state_backend.py` creates
|
||||
the S3 + DynamoDB state backend (idempotent). Mirrors the reference's
|
||||
bootstrap script shape.
|
||||
- **REQ-15:** `terraform/bootstrap/create_iam_user.py` creates the
|
||||
runner IAM user + policy (idempotent). Prints the initial key.
|
||||
- **REQ-16:** `terraform/ci-vpc/main.tf` defines the shared platform
|
||||
VPC used by all stacks.
|
||||
- **REQ-17:** `terraform/platform/main.tf` defines platform-level
|
||||
resources (state bucket references, runner role).
|
||||
- **REQ-18:** `terraform/microservice/main.tf` is a sample consumer-
|
||||
facing Terraform root that the microservice L2 pattern deploys into.
|
||||
- **REQ-19:** `terraform/onboarding/main.tf` defines the onboarding
|
||||
stack (creates a consumer's IAM role scoped to their repo tags).
|
||||
Simplified from the reference (no OIDC — static key alternative
|
||||
only, documented in README).
|
||||
|
||||
## Category: Local Shell Reproducibility (feat)
|
||||
|
||||
- **REQ-20:** `scripts/run_platform.sh` orchestrates the full
|
||||
pipeline: contract → resolver → adapter → security (skipped —
|
||||
no policy layer) → plan → apply. Flags: `--check-only` (offline,
|
||||
contract → resolver → adapter → structure validation, exits 0 on
|
||||
success), `--plan-only` (no apply), `--quiet` (suppress streaming),
|
||||
`--help`. Default mode (no flag) applies. Streams output by default.
|
||||
- **REQ-21:** `scripts/run_ci.sh` mirrors a CI pipeline locally:
|
||||
lint (py_compile) → test (pytest) → check-only
|
||||
(`run_platform.sh --check-only`). Three stages in sequence.
|
||||
`--quiet` suppresses banners.
|
||||
- **REQ-22:** `scripts/rotate_spike_key.sh` rotates the runner key
|
||||
into `.env.secrets` (gitignored, chmod 600). Mirrors the reference.
|
||||
|
||||
## Category: Offline Test Suite (feat)
|
||||
|
||||
- **REQ-23:** `tests/test_contract_resolver.py` — unit tests for
|
||||
`resolve()` covering happy path, unknown module, unknown version,
|
||||
empty infrastructure array.
|
||||
- **REQ-24:** `tests/test_environment_check.py` — unit tests for
|
||||
`check()` covering existing env, missing env, malformed env file.
|
||||
- **REQ-25:** `tests/test_terraform_adapter.py` — unit tests for
|
||||
`adapt()` covering single-resource stack, multi-resource stack,
|
||||
input passthrough, HCL syntax validity.
|
||||
- **REQ-26:** `tests/test_engine_boundary.py` — grep-based test for
|
||||
REQ-09 (no engine terms outside `adapters/terraform/`).
|
||||
- **REQ-27:** `tests/test_contract_schema.py` — validates sample
|
||||
contracts against `schemas/contract.schema.json` using `jsonschema`.
|
||||
- **REQ-28:** `tests/test_stack_schema.py` — validates resolved stacks
|
||||
against `schemas/stack.schema.json`.
|
||||
- **REQ-29:** `tests/test_run_platform_check_only.py` — invokes
|
||||
`scripts/run_platform.sh --check-only` and asserts exit 0 + the
|
||||
"PLATFORM CHECK OK" banner. Offline (uses local emulators / moto).
|
||||
- **REQ-30:** `tests/test_run_ci.sh` — invokes `scripts/run_ci.sh`
|
||||
and asserts exit 0 + the "CI PIPELINE OK" banner.
|
||||
- **REQ-31:** `requirements-test.txt` pins `pytest`, `moto`, `jsonschema`,
|
||||
`boto3`, `pyyaml`. `pyproject.toml` configures pytest + py_compile.
|
||||
|
||||
## Category: Documentation (feat)
|
||||
|
||||
- **REQ-32:** `README.md` covers: what the platform is, how to run
|
||||
offline (`run_platform.sh --check-only`), how to run the test suite,
|
||||
how to run against live AWS, repository layout table, credentials
|
||||
(static key alternative only — no OIDC), consumer guide pointer.
|
||||
- **REQ-33:** `docs/architecture.md` is the source of truth for how
|
||||
the platform works (mirrors `.ciagent/ARCHITECTURE.md`).
|
||||
- **REQ-34:** `docs/modules/` documents each L1 primitive + L2 pattern
|
||||
(one .md per module, same shape as the reference).
|
||||
- **REQ-35:** `docs/contracts/` documents the contract schema + sample
|
||||
contracts.
|
||||
- **REQ-36:** `docs/environments/` documents the environment model +
|
||||
the sample dev environment.
|
||||
- **REQ-37:** `docs/consumer-guide.md` is the step-by-step guide for
|
||||
a consumer to write a contract and deploy (infra-only — no security
|
||||
sections).
|
||||
- **REQ-38:** `.gitignore` seeds `.env`, `.env.secrets`, `.env.*`,
|
||||
terraform state, credentials, `__pycache__/`, `.ciagent/logs/`.
|
||||
|
||||
## Out of scope (locked — do NOT implement in v1.0)
|
||||
|
||||
- Security/policy: kyverno-json, Wiz, Checkov custom rules,
|
||||
`PolicyEngine`, `PolicyCheckResult` gating, `core/policy_engine.py`.
|
||||
- Confidence + evidence: `core/confidence_signal.py`,
|
||||
`core/outbox_writer.py`, audit ledger, attestation matrix.
|
||||
- Identity/ABAC: Nova-idp, PAT lifecycle, `core/abac_evaluator.py`,
|
||||
`core/auth_store.py`, `core/jws_attestation.py`, `core/kms_signing.py`,
|
||||
`core/pat_lifecycle.py`, `core/separation_of_duties.py`,
|
||||
`core/hitl_gates.py`, `core/attestation_matrix.py`,
|
||||
`core/submission_readiness.py`.
|
||||
- CI/CD pipeline: `.github/workflows/ci.yml`,
|
||||
`.github/workflows/deploy.yml`, `pipelines/`,
|
||||
`schemas/pipeline.schema.json`, `schemas/deploy-pipeline.schema.json`.
|
||||
- Metrics/telemetry: `metrics/`, `core/metrics/`,
|
||||
`core/regression_verify*.py`.
|
||||
- Leadership decks, PPTX, marp slides.
|
||||
- Decommission alias, env_transition, mode_resolver, onboarding flow
|
||||
beyond bootstrap.
|
||||
- Multi-project mode, consumer subprojects.
|
||||
- OIDC federation (plain static AWS key for dev only).
|
||||
@@ -0,0 +1,331 @@
|
||||
# RESEARCH — Nova Platform v1.0
|
||||
|
||||
> Phase 0 RESEARCH artifact. Derived from structural analysis of the Nova
|
||||
> reference at `/home/opencode/acdl/`. Each section documents the
|
||||
> reference's exact shape so execution phases can mirror it, then notes
|
||||
> the nova-platform adaptation referencing the locked decisions
|
||||
> (D-001..D-035 in `.ciagent/CLARIFY.md` + `.ciagent/PROJECT.md`).
|
||||
>
|
||||
> **Scope rule:** security/audit/identity/CI-workflow machinery is OUT OF
|
||||
> SCOPE. Files related to those subsystems were NOT read. This document
|
||||
> covers only the in-scope infrastructure-delivery core.
|
||||
|
||||
---
|
||||
|
||||
## 1. `schemas/contract.schema.json` — contract envelope
|
||||
|
||||
### Reference shape
|
||||
- JSON Schema draft 2020-12. `$id: https://nova.cloudinit.dev/schemas/contract.schema.json`. Title `Nova Consumer Contract`.
|
||||
- Top-level `type: object`, `required: ["id", "name", "environment", "infrastructure"]`, `additionalProperties: false`.
|
||||
- `id`: `type: string`, `pattern: ^[a-z][a-z0-9-]{2,5}$` (3-6 char operational acronym).
|
||||
- `name`: `type: string`, `minLength: 3` (human-readable).
|
||||
- `environment`: `type: string`, `enum: [dev, qa, prod, dr]`.
|
||||
- `infrastructure`: **OBJECT** (map), `minProperties: 1`, `additionalProperties: false`. `patternProperties` keyed by `^[a-z][a-z0-9-]*$` (module name). Each entry is an object `required: ["inputs"]` with `version` (optional, semver `^\d+\.\d+\.\d+$`) and `inputs` (object, `additionalProperties` allowing string/number/boolean/object/array), `additionalProperties: false` on the entry.
|
||||
- The contract is the ONLY consumer surface. Engine-agnostic: no `aws_*` terms in the schema keywords.
|
||||
|
||||
### Nova-platform adaptation (REQ-01 + D-015)
|
||||
- Same draft 2020-12, same `$id` host, same title pattern.
|
||||
- Same `id` pattern, same `name` minLength, same `environment` enum.
|
||||
- **KEY DEVIATION:** `infrastructure` is an **ARRAY** (per REQ-01 + D-015), not the reference's object map. `type: array`, `minItems: 1`. Each item is an object `required: ["module", "inputs"]` (NOT keyed by module name — the module name is a field). Fields per item: `module` (string, required), `version` (string, optional, semver pattern — defaults to latest non-deprecated per D-015), `inputs` (object, required, `additionalProperties: false` allowing primitives/objects/arrays).
|
||||
- No `aws_*` or engine terms in the schema. Validated by `tests/test_contract_schema.py` (REQ-27).
|
||||
|
||||
---
|
||||
|
||||
## 2. `schemas/stack.schema.json` — resolved stack shape
|
||||
|
||||
### Reference shape
|
||||
- Draft 2020-12. Title `Nova Target Stack`. `required: ["version", "stack", "resources"]`.
|
||||
- `version`: semver string. `stack`: object `required: ["name", "kind", "depth"]`.
|
||||
- `resources`: array of `$defs/resource` — each `required: ["id", "type", "module", "inputs"]`. `id` pattern `^[a-z][a-z0-9-]*$`. `type` is stack-typed (`aws:s3:bucket`, NOT `aws_s3_bucket`). `module` is `name@semver`. Optional `parent`, `outputs`, `nfrs`.
|
||||
- Optional `relationships` array. Schema body is engine-agnostic.
|
||||
|
||||
### Nova-platform adaptation (REQ-04 + D-012)
|
||||
- **FLAT shape** per D-012. `required: ["contract_id", "contract_name", "environment", "resources"]`.
|
||||
- `contract_id`, `contract_name` (strings), `environment` (string enum).
|
||||
- `resources`: array of `{module, version, source, inputs}` where `source` is a Terraform module path (engine terms appear HERE only — the stack is the resolved form passed to the adapter, NOT the contract).
|
||||
- NO `stack` wrapper, NO `relationships`, NO `nfrs`, NO `data_sources`, NO `outputs` map. L2 is opaque per D-012.
|
||||
|
||||
---
|
||||
|
||||
## 3. `schemas/environment.schema.json` — environment record
|
||||
|
||||
### Reference shape
|
||||
- Draft 2020-12. `required: ["name", "account_id", "region", "state_backend", "network", "runner_role_arn", "autonomy", "confidence_threshold"]`.
|
||||
- `account_id`: 12-digit pattern (placeholder `000000000000` allowed). `state_backend`: `{bucket, lock_table}`. `network`: `{vpc_cidr, azs}`. `runner_role_arn`, `autonomy` enum, `confidence_threshold` 0-1.
|
||||
- `additionalProperties: false` on top level.
|
||||
|
||||
### Nova-platform adaptation (D-017 + D-018)
|
||||
- **KEEP** the schema (D-017) but **simplify** the required field set per D-018.
|
||||
- `required: ["name", "account_id", "region", "state_backend", "network"]`.
|
||||
- **DROP** `runner_role_arn` (ABAC OOS), `autonomy` (HITL OOS), `confidence_threshold` (OOS).
|
||||
- Keep `description` optional, `account_id` 12-digit pattern + placeholder warning, `region`, `state_backend {bucket, lock_table}`, `network {vpc_cidr, azs}`. `additionalProperties: false`.
|
||||
|
||||
---
|
||||
|
||||
## 4. `core/contract_resolver.py` — resolve() function
|
||||
|
||||
### Reference shape
|
||||
- `resolve(contract_path, repo_root=None, environment_override=None) -> dict`. Takes a **file path** to contract YAML. Loads YAML, loads env via `environment_check.load()`, builds interpolation context `{"env": env, "contract": contract}`. Expands `${env.*}` / `${contract.*}` AFTER schema validation, BEFORE IR resolution.
|
||||
- `_TOKEN_RE = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")`. `_lookup_dotted(context, dotted)`. `_expand_vars(value, context)` recurses; unknown token raises `ValueError`.
|
||||
- `_latest_version(registry, module_name)`. `_resolve_l1(...)` builds resource from interface.json. `_resolve_l2(...)` loads composition.json, expands children/wires/data_sources.
|
||||
- Exceptions: generic `ValueError` strings (no custom classes). CLI delegates to `contract_resolver_cli.main`.
|
||||
|
||||
### Nova-platform adaptation (REQ-03 + D-011 + D-016)
|
||||
- **Signature per D-011:** `resolve(contract: dict, registry: dict, modules_dir: Path) -> dict`. Takes a **validated contract dict** (not a path), a **registry dict**, and a **modules_dir Path**. "Pure" = no network/side-effects; local file reads for module metadata ARE permitted.
|
||||
- **Interpolation KEPT** (D-016): `_TOKEN_RE`, `_lookup_dotted`, `_expand_vars` preserved verbatim (engine-agnostic). Unknown token raises `ValueError`.
|
||||
- **Named exceptions per REQ-03:** `ModuleNotFoundError` (unknown module), `VersionNotFoundError` (unknown version) — REPLACE the reference's generic `ValueError` strings.
|
||||
- **L2 = opaque per D-012:** NO `_resolve_l2` composition expansion. L2 is a single stack resource `{module, version, source, inputs}`. NO children/wires/data_sources.
|
||||
- **NO policy evaluation, NO CLI module.** Run as script. Returns the flat stack dict per REQ-04.
|
||||
|
||||
---
|
||||
|
||||
## 5. `core/environment_check.py` — check()/load()
|
||||
|
||||
### Reference shape
|
||||
- `load(env_name, root=None) -> dict` — raises `FileNotFoundError`. `check(contract_path, env_name, root) -> (ok, message)` tuple. `_onboarding_message(env_name)` friendly prompt. `main(argv)` CLI.
|
||||
|
||||
### Nova-platform adaptation (REQ-05 + D-019)
|
||||
- **Signature per D-019:** `check(env_name: str, environments_dir: Path) -> dict`. Returns the **env dict** directly. Raises `EnvironmentNotFoundError` on missing env.
|
||||
- **DROP** `_onboarding_message`, `contract_path` param, `main()` CLI. `load()` folded into `check()` or kept as internal helper.
|
||||
|
||||
---
|
||||
|
||||
## 6. `adapters/terraform/` — the Terraform adapter
|
||||
|
||||
### Reference shape
|
||||
- `adapters/terraform/adapter.py` + `policy/` (OOS). **NO `__init__.py`**.
|
||||
- `adapt(stack_instance, out_dir)` — emits THREE files: `main.tf` (module blocks + data blocks + root outputs), `terraform.tf` (required_version + required_providers + s3 backend env-scoped key), `providers.tf` (`provider "aws"`).
|
||||
- `_tf_value`, `_ref_expr`, `_module_name`, `_emit_module_block`, `_emit_root_output`. Multi-resource L1 dedup (`_child_id`).
|
||||
- Statelessness guards: no TYPE_MAP/INPUT_MAP/OUTPUT_MAP, < 250 lines.
|
||||
|
||||
### Nova-platform adaptation (REQ-07 + REQ-08 + D-013)
|
||||
- **File layout:** `adapters/terraform/adapter.py` + `adapters/terraform/__init__.py` (re-exports `adapt`). **NO `policy/`**.
|
||||
- **Signature per REQ-07:** `adapt(stack: dict, modules_dir: Path) -> str`. Returns HCL string (caller writes main.tf). Stateless assembler — no terraform invocation, no state, no plan.
|
||||
- Emits `module "x" { source = ...; <inputs> }` per resource. L2 `source` = `modules/l2/<name>/terraform` (D-013). `ref:` translation + multi-resource dedup SIMPLIFIED (L2 opaque, flat stack has no refs).
|
||||
- `region` skip + provider-level region pattern preserved. terraform.tf + providers.tf emitted by small helper or `run_platform.sh`.
|
||||
- **Engine boundary:** ONLY place `aws_*` / `terraform` / `module "` / `provider "` / `resource "` appear (REQ-09).
|
||||
|
||||
---
|
||||
|
||||
## 7. `modules/registry.json` — module index
|
||||
|
||||
### Reference shape
|
||||
- Top-level object keyed by module name → version string → entry. L1: `{interface, terraform_dir, published_at, deprecated, kind:"l1"}`. L2: `{interface, published_at, deprecated, kind:"l2"}` — **NO `terraform_dir`**.
|
||||
- 13 L1 + 2 L2 = 15 entries.
|
||||
|
||||
### Nova-platform adaptation (REQ-10 + D-013)
|
||||
- **Matches reference shape exactly** per REQ-10.
|
||||
- **DEVIATION per D-013:** L2 entries DO include `terraform_dir: "modules/l2/<name>/terraform"` (required by flat stack's `source` field).
|
||||
- L2 `interface` points at `modules/l2/<name>/interface.json` (NOT `composition.json`). Same 15 entries, same names.
|
||||
|
||||
---
|
||||
|
||||
## 8. `modules/l1/s3/` — representative L1 primitive
|
||||
|
||||
### Reference shape
|
||||
- `interface.json`: `{name, version, kind:"l1", type, description, inputs, outputs, nfrs, resources?, intra_refs?}`. `type` stack-typed (`aws:s3:bucket`). `inputs`/`outputs` keyed by name → `{type, description, required?, default?}`.
|
||||
- `terraform/`: `main.tf`, `variables.tf`, `outputs.tf`, `versions.tf`, `locals.tf`. `count = var.enabled ? 1 : 0`. `required_version = ">= 1.9, < 1.10"`. `aws = { source = "hashicorp/aws"; version = "~> 5.0" }`.
|
||||
- `README.md` (follows README-TEMPLATE.md), `instance.json`, `examples/`.
|
||||
|
||||
### Nova-platform adaptation (REQ-11 + D-014)
|
||||
- `interface.json` per D-014: KEEP `name, version, kind, type, description, inputs, outputs, resources` (multi-resource array for vpc/ecs-service/alb). **DROP `nfrs`** (confidence signal OOS) and **DROP `intra_refs`** (wire engine eliminated by D-012).
|
||||
- `terraform/` shape preserved (5 files, same HCL conventions). All 13 L1 primitives authored.
|
||||
- Per D-035: L1 `s3` stays reference interface (`bucket_name`/`region`/`kms_key_arn`/`enabled`) — does NOT gain `index_document`.
|
||||
|
||||
---
|
||||
|
||||
## 9. `modules/l2/microservice/` — L2 pattern
|
||||
|
||||
### Reference shape
|
||||
- `composition.json`: `{name, version, kind:"l2", depth, children[], data_sources[], wires[], outputs[]}`. ~24 wires. **NO `terraform/` directory** (resolver expands; adapter emits per-L1 blocks).
|
||||
|
||||
### Nova-platform adaptation (REQ-12 + D-012 + D-013)
|
||||
- **`interface.json` replaces `composition.json`** per D-013. Content: `{name, version, kind:"l2", description, inputs, outputs}` — L2-level only, NO children/wires.
|
||||
- **`terraform/main.tf` ADDED** per D-012: composes L1 internally via `module` blocks. Resolver treats L2 as single resource; adapter emits one `module "microservice" { source = "modules/l2/microservice/terraform" }` block. L2's `main.tf` instantiates `module "cluster" { source = "../../l1/ecs-cluster/terraform" }` etc.
|
||||
- 2 L2 patterns: microservice (vpc + ecs-cluster + ecs-service + iam-role + ecr + alb), static-assets (s3 + cloudfront + kms-key).
|
||||
|
||||
---
|
||||
|
||||
## 10. `scripts/run_platform.sh` — platform pipeline orchestrator
|
||||
|
||||
### Reference shape
|
||||
- `set -euo pipefail`. Flag parsing: `--check-only`, `--plan-only`, `--apply`, `--destroy`, `--quiet`, `--deploy-uptime`, `--decommission`, `--local`, `--environment`, `--help`.
|
||||
- Stages: env check → validate contract → resolve → adapter → [check-only: validate output → exit 0] → load AWS creds (NOVA_AWS_* → AWS_*) → terraform init/validate/plan → [plan-only: exit 0] → apply → E2E OK.
|
||||
- Banners: `=== PLATFORM CHECK OK ===`, `=== PLATFORM PLAN OK ===`, `=== PLATFORM APPLY OK ===`, `=== PLATFORM E2E OK ===`.
|
||||
|
||||
### Nova-platform adaptation (REQ-20 + D-020 + D-031 + D-032)
|
||||
- **Flags per D-031:** ONLY `--check-only`, `--plan-only`, `--quiet`, `--help` (+`-h`). Default = apply. DROP all others.
|
||||
- **Stages per D-020:** check-only (offline): env_check → validate → resolve → adapter → validate output → `=== PLATFORM CHECK OK ===`. plan-only: + creds → init/validate/plan → `=== PLATFORM PLAN OK ===`. default: + apply → `=== PLATFORM APPLY OK ===`.
|
||||
- **AWS creds per D-032:** `NOVA_AWS_*` prefix → `AWS_*` copy, then unset `NOVA_AWS_*`.
|
||||
- DROP: env_transition, Checkov, kyverno-json, confidence, HITL, outbox, output publisher, uptime, decommission, local emulators.
|
||||
|
||||
---
|
||||
|
||||
## 11. `scripts/run_ci.sh` — local CI pipeline mirror
|
||||
|
||||
### Reference shape
|
||||
- 3 stages: lint (py_compile, hardcoded file list) → test (pytest) → check-only. `=== CI PIPELINE OK ===`.
|
||||
|
||||
### Nova-platform adaptation (REQ-21 + D-021)
|
||||
- Same 3-stage flow. **Stage 1 per D-021:** glob `python3 -m py_compile $(find core/ adapters/ scripts/ -name '*.py')` (no hardcoded list — no OOS Python files exist).
|
||||
- Banners preserved. `--quiet` suppresses banners.
|
||||
|
||||
---
|
||||
|
||||
## 12. `terraform/bootstrap/` — state backend + IAM user scripts
|
||||
|
||||
### Reference shape
|
||||
- `create_state_backend.py`: S3 `nova-tfstate-<account>-us-east-1` + DynamoDB `nova-outbox`. Idempotent. `NOVA_BOOTSTRAP_AWS_*` (fallback `NOVA_AWS_*`).
|
||||
- `create_iam_user.py`: IAM user `nova-spike-runner` + inline policy + key. Prints `NOVA_AWS_*`.
|
||||
- `spike_runner_policy.json`: hardcoded account `581513795199`.
|
||||
|
||||
### Nova-platform adaptation (REQ-14 + REQ-15 + D-022 + D-026)
|
||||
- `create_state_backend.py` per D-022: S3 `nova-tfstate-<account>-<region>` + **DynamoDB `nova-tfstate-locks`** (NOT `nova-outbox`). Idempotent.
|
||||
- `create_iam_user.py` per D-026: user `nova-spike-runner` + policy + key. Account ID **parameterized** (NOT hardcoded).
|
||||
- `spike_runner_policy.json`: account parameterized. Grants S3/DynamoDB lock/ECS/ECR/ELB/IAM/EC2/CloudFront/WAF/KMS. **DROP** Lambda, Secrets, SNS, CostExplorer, OIDC.
|
||||
|
||||
---
|
||||
|
||||
## 13. `terraform/platform/main.tf` — platform infrastructure
|
||||
|
||||
### Reference shape
|
||||
- 367 lines: KMS, DynamoDB contracts, Secrets, Lambda, SNS, **shared VPC**, outputs `vpc_id`/`subnet_ids`/`ecs_security_group_id`.
|
||||
|
||||
### Nova-platform adaptation (REQ-17 + D-023)
|
||||
- **ONLY shared platform VPC** per D-023: `aws_vpc.nova_shared`, `aws_subnet.nova_shared` (count=2), IGW, route table, ECS SG. Outputs `vpc_id`, `subnet_ids`, `ecs_security_group_id`.
|
||||
- **DROP** Lambda, DynamoDB, KMS, Secrets, SNS, consumer_invoke_policy — ALL OOS.
|
||||
|
||||
---
|
||||
|
||||
## 14. `terraform/ci-vpc/main.tf` — short-lived test VPC
|
||||
|
||||
### Reference shape
|
||||
- Short-lived VPC for module lifecycle testing. VPC + 2 subnets + IGW + route table + ECS SG + ECS cluster. Outputs `vpc_id`/`subnet_ids`/`ecs_security_group_id`/`cluster_arn`. State key `spike/ci-vpc/terraform.tfstate`.
|
||||
|
||||
### Nova-platform adaptation (REQ-16 + D-024)
|
||||
- **Preserved** per D-024. Short-lived test VPC. Shared platform VPC lives in `terraform/platform/main.tf`. Same shape.
|
||||
|
||||
---
|
||||
|
||||
## 15. `terraform/onboarding/main.tf` — consumer onboarding stack
|
||||
|
||||
### Reference shape
|
||||
- IAM role `nova-<consumer_repo>-deploy` with **OIDC** trust. Inline policy `lambda:InvokeFunctionUrl` (ABAC). Outputs `consumer_deploy_role_arn`/`consumer_deploy_role_name`.
|
||||
|
||||
### Nova-platform adaptation (REQ-19 + D-025)
|
||||
- **IAM ROLE (not user)** per D-025 (human override). Trust policy allows **platform's runner user** to assume it (**cross-account assume role**, `sts:AssumeRole`). **NO OIDC**.
|
||||
- `consumer_repo`/`owner_id` vars for tagging. Inline policy: Terraform-deployable permissions scoped via tags. **DROP** `lambda:InvokeFunctionUrl`.
|
||||
- Outputs `consumer_deploy_role_arn`/`consumer_deploy_role_name`. README documents dev-only static-key.
|
||||
|
||||
---
|
||||
|
||||
## 16. `terraform/microservice/main.tf` — sample consumer Terraform root
|
||||
|
||||
### Reference shape
|
||||
- 147 lines. Hand-authored root: VPC, subnets, ECS cluster, ECR, IAM role, ALB, listener, task def, ECS service. Companion `terraform.tf` + `providers.tf`.
|
||||
|
||||
### Nova-platform adaptation (REQ-18)
|
||||
- Preserved as sample consumer-facing root. **Simplified** to opaque L2 model (D-012): L2's own `modules/l2/microservice/terraform/main.tf` composes L1 via `module` blocks. `terraform/microservice/main.tf` instantiates the L2 module + wires to platform VPC via `data.terraform_remote_state`.
|
||||
- State key `spike/microservice/<env>/terraform.tfstate` (env-scoped).
|
||||
|
||||
---
|
||||
|
||||
## 17. `tests/` — test file naming + structure
|
||||
|
||||
### Reference shape
|
||||
- `conftest.py`: `ROOT` + `sys.path.insert`. Fixtures: `repo_root`, `stack_instance`, `stack_schema`, `registry`, `policy_check_result_schema`.
|
||||
- `test_contract_resolver.py`, `test_environment_check.py`, `test_adapter.py` (classes for instance, registry, module assembly, ref expr, tf value, statelessness, valid terraform, dedup).
|
||||
|
||||
### Nova-platform adaptation (REQ-23..REQ-30)
|
||||
- `conftest.py`: `repo_root`, `stack_schema`, `registry`. **DROP** `stack_instance` + `policy_check_result_schema`.
|
||||
- `test_contract_resolver.py` (REQ-23): `resolve(contract, registry, modules_dir)` — happy path, `ModuleNotFoundError`, `VersionNotFoundError`, empty infrastructure.
|
||||
- `test_environment_check.py` (REQ-24): `check(env_name, environments_dir)` — existing env, `EnvironmentNotFoundError`, malformed.
|
||||
- `test_terraform_adapter.py` (REQ-25): `adapt(stack, modules_dir)` — single/multi resource, input passthrough, HCL validity.
|
||||
- `test_engine_boundary.py` (REQ-26 + D-034): grep `.py` files only (core/schemas/contracts/tests/scripts/root), exclude `adapters/terraform/`/modules/.tf/.md/.json.
|
||||
- `test_contract_schema.py` (REQ-27), `test_stack_schema.py` (REQ-28), `test_run_platform_check_only.py` (REQ-29), `test_run_ci.sh` (REQ-30).
|
||||
|
||||
---
|
||||
|
||||
## 18. `pyproject.toml` — pytest config + project metadata
|
||||
|
||||
### Reference shape
|
||||
- `[project] name = "nova"`, `[project.scripts] nova = "nova.cli:main"`. test deps include pytest-cov, pytest-json-report, hypothesis. `addopts` writes to `metrics/`.
|
||||
|
||||
### Nova-platform adaptation (REQ-31 + D-027)
|
||||
- **No CLI package** per D-027. `[project] name = "nova-platform"`. **NO `[project.scripts]`**. NO `nova/` dir.
|
||||
- test deps: `pytest>=8.0, moto[dynamodb]>=5.0, jsonschema>=4.20, pyyaml>=6.0, boto3>=1.34`. **DROP** pytest-cov, pytest-json-report, hypothesis.
|
||||
- `addopts = "-v --tb=short"` (no metrics/). `markers = [offline, slow]`. `[tool.setuptools.packages.find]` includes `core, core.*, adapters.*` (NO `nova`).
|
||||
|
||||
---
|
||||
|
||||
## 19. `requirements-test.txt`
|
||||
|
||||
```
|
||||
pytest>=8.0
|
||||
moto[dynamodb]>=5.0
|
||||
jsonschema>=4.20
|
||||
pyyaml>=6.0
|
||||
boto3>=1.34
|
||||
```
|
||||
|
||||
(DROP `pytest-cov` — no coverage reporting.)
|
||||
|
||||
---
|
||||
|
||||
## 20. `.gitignore`
|
||||
|
||||
Already seeded in nova-platform (matches reference minus OOS metrics lines). Contains `__pycache__/`, `.env*`, terraform state, credentials, `.ciagent/logs/`, `nova_platform.egg-info/`.
|
||||
|
||||
---
|
||||
|
||||
## 21. `modules/README.md` + `modules/README-TEMPLATE.md`
|
||||
|
||||
### Reference shape
|
||||
- `README.md` (63 lines): Primitives vs Modules, tables, registry, template link. `README-TEMPLATE.md` (62 lines): Overview/Resources/Inputs/Outputs/NFRs/Usage/Compliance/Versioning. `STANDARDS.md` (673 lines — security/compliance).
|
||||
|
||||
### Nova-platform adaptation (REQ-13 + D-029)
|
||||
- `modules/README.md`: L1/L2 distinction, registry format, how to add a module. **Trimmed of security/attestation**. 13-row primitives table, 2-row modules table.
|
||||
- `modules/README-TEMPLATE.md`: KEEP. Sections: Overview/Resources/Inputs/Outputs/Usage/Versioning. **DROP NFRs + Compliance** sections.
|
||||
- **DROP `modules/STANDARDS.md`** per D-029.
|
||||
|
||||
---
|
||||
|
||||
## 22. `docs/` — documentation shapes
|
||||
|
||||
### Reference shape
|
||||
- `docs/modules/index.md` (catalog → links to `modules/*/README.md`). `docs/contracts/index.md` (fields table, samples). `docs/environments/index.md` (env model, autonomy table). `docs/consumer-guide.md` (513 lines, 9 steps). `docs/architecture.md` (241 lines, 4 layers + cross-cutting).
|
||||
|
||||
### Nova-platform adaptation (REQ-32..REQ-37)
|
||||
- `docs/modules/index.md` (REQ-34 + D-028): catalog table linking to `modules/*/README.md` (per-module docs live in `modules/`, not `docs/modules/`).
|
||||
- `docs/contracts/index.md` (REQ-35): array-based `infrastructure` schema + samples + per-env variants (D-033).
|
||||
- `docs/environments/index.md` (REQ-36): env model (account/network/state backend — NO IAM role/ABAC). Autonomy table simplified (dev autonomous; qa/prod/dr manual operator — NO HITL gates).
|
||||
- `docs/consumer-guide.md` (REQ-37): infra-only. Steps: create repo, write contract, run check-only, run against AWS. Keep interpolation table. DROP OIDC/reusable-workflow/decommission/compliance.
|
||||
- `docs/architecture.md` (REQ-33): mirrors `.ciagent/ARCHITECTURE.md`. Four layers + engine boundary + OOS list + module catalog. NO cross-cutting concerns sections.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting observations
|
||||
|
||||
### Engine-agnostic invariant
|
||||
`schemas/contract.schema.json` + `schemas/stack.schema.json` + `core/contract_resolver.py` + `core/environment_check.py` contain NO `aws_*` / Terraform terms. ONLY `adapters/terraform/` is engine-specific. Verified by `tests/test_engine_boundary.py` (D-034 — grep `.py` only).
|
||||
|
||||
### Structural conventions preserved without deviation
|
||||
- Directory names: `schemas/`, `core/` (+ `core/environments/`), `adapters/terraform/`, `modules/` (`l1/`, `l2/`, `registry.json`), `contracts/`, `scripts/`, `terraform/` (`bootstrap/`, `ci-vpc/`, `microservice/`, `onboarding/`, `platform/`), `tests/`, `docs/`.
|
||||
- File roles: `interface.json`, `terraform/main.tf` (+ variables/outputs/versions/locals), `registry.json` entry shape, schemas, shell scripts, bootstrap scripts.
|
||||
- Module interface shape: `{name, version, kind, type, description, inputs, outputs}` + `resources[]` for multi-resource L1s. Stack-typed `type` (`aws:s3:bucket`).
|
||||
- Registry entry shape: `{interface, terraform_dir, published_at, deprecated, kind}`.
|
||||
- Banner strings: `=== PLATFORM CHECK OK ===`, `=== PLATFORM PLAN OK ===`, `=== PLATFORM APPLY OK ===`, `=== CI PIPELINE OK ===`.
|
||||
- State key convention: `spike/<stack_name>/<environment>/terraform.tfstate` (env-scoped).
|
||||
- Tag convention: `nova:owner`, `nova:contract`, `nova:environment`, `nova:cost-center`.
|
||||
|
||||
### Key deviations (locked in CLARIFY.md)
|
||||
- **D-012:** L2 is opaque (single stack resource, no children/wires expansion). Flat stack.
|
||||
- **D-013:** L2 uses `interface.json` (not `composition.json`) + `terraform_dir` in registry.
|
||||
- **D-015:** `infrastructure` is an ARRAY (not object map); `version` optional.
|
||||
- **D-017/D-018:** environment schema simplified (drops runner_role_arn/autonomy/confidence_threshold).
|
||||
- **D-019:** `check()` returns dict, raises `EnvironmentNotFoundError` (not tuple).
|
||||
- **D-022:** lock table `nova-tfstate-locks` (not `nova-outbox`).
|
||||
- **D-023:** `terraform/platform/main.tf` = ONLY shared VPC.
|
||||
- **D-025:** onboarding = IAM role cross-account assume (not OIDC, not user).
|
||||
- **D-027:** no CLI package (scripts only).
|
||||
- **D-029:** drop `STANDARDS.md`.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Nova Platform — Roadmap
|
||||
|
||||
> Skeleton. The roadmapper refines phase detail during `/ci-run` phase 0.
|
||||
> Init establishes the milestone + phase count estimate.
|
||||
|
||||
## Milestone v1.0 — Nova Platform Inaugural
|
||||
|
||||
> **Type:** major (first release, no prior tags per D-001). The final
|
||||
> phase's tag IS the initial release (`v1.0.0`). Tags run on the v0.x
|
||||
> patch line — but since there is no prior minor, the first milestone's
|
||||
> phase 0 ships `v0.1.0`, execution phases ship `v0.1.1`..`v0.1.N`, and
|
||||
> the final phase ships `v1.0.0` (major bump).
|
||||
>
|
||||
> **Branch:** `milestone/v1.0-nova-platform` (already created in init).
|
||||
> **Phase 0:** `phase/00-pre-execution` (current).
|
||||
|
||||
### Phase 0 — Pre-Execution (current)
|
||||
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → GRILL → MVP/UX CHECK → SHIP.
|
||||
|
||||
Produces all `.ciagent/` markdown (PROJECT.md, ARCHITECTURE.md,
|
||||
ROADMAP.md, REQUIREMENTS.md, PERSONAS.md, PLAN.md, GRILL.md), research
|
||||
files, and the task-level plan. Ships as `v0.1.0`.
|
||||
|
||||
### Phase 1 — Contract Surface + Schemas + Resolver
|
||||
|
||||
Implements REQ-01..06 + REQ-23..28 (contract schema, stack schema,
|
||||
resolver, environment check, their tests). First vertical slice: a
|
||||
contract can be validated, resolved to a stack, and the stack validated
|
||||
— offline, no apply. Ships as `v0.1.1`.
|
||||
|
||||
### Phase 2 — Terraform Adapter + Engine Boundary
|
||||
|
||||
Implements REQ-07..09 + REQ-25..26 (adapter, engine-boundary test).
|
||||
Second slice: a stack can be compiled to Terraform HCL — offline, no
|
||||
apply. Ships as `v0.1.2`.
|
||||
|
||||
### Phase 3 — L1 Primitives + Registry
|
||||
|
||||
Implements REQ-10..11 (registry + all 13 L1 primitives). Third slice:
|
||||
the module catalog exists; the adapter can emit real module blocks.
|
||||
Ships as `v0.1.3`.
|
||||
|
||||
### Phase 4 — L2 Patterns + Terraform Bootstrap + Platform
|
||||
|
||||
Implements REQ-12 + REQ-14..19 (L2 patterns, bootstrap scripts,
|
||||
platform/microservice/onboarding Terraform). Fourth slice: the
|
||||
reference deployable patterns exist; AWS bootstrap is scripted. Ships
|
||||
as `v0.1.4`.
|
||||
|
||||
### Phase 5 — Shell Reproducibility + Test Suite + Docs
|
||||
|
||||
Implements REQ-20..22 + REQ-29..31 + REQ-32..38 (run_platform.sh,
|
||||
run_ci.sh, rotate_spike_key.sh, full test suite, README, docs/).
|
||||
Fifth slice: the platform is fully reproducible from the shell and
|
||||
documented. Ships as `v0.1.5`.
|
||||
|
||||
### Phase 6 — Final Review + Ship (milestone release)
|
||||
|
||||
REVIEW + AUDIT + milestone SHIP. Merges `phase/06` →
|
||||
`milestone/v1.0-nova-platform` → `main`. Tags `v0.1.6` (the milestone
|
||||
release on the v0.1 patch line — v1.0 is the milestone *label*, tags
|
||||
run on the previous minor's patch line per branch-strategy.md; since
|
||||
there is no prior minor, v0.1.x is the patch line and v0.1.6 IS the
|
||||
v1.0 milestone release). Creates the Gitea release. Deletes all
|
||||
milestone branches.
|
||||
|
||||
## Coverage (init estimate — refined by PLAN)
|
||||
|
||||
| REQ-IDs | Phase |
|
||||
|---------|-------|
|
||||
| (none — P0 is pre-execution) | 0 |
|
||||
| REQ-01..06, 23..28 | 1 |
|
||||
| REQ-07..09, 25..26 | 2 |
|
||||
| REQ-10..11 | 3 |
|
||||
| REQ-12, 14..19 | 4 |
|
||||
| REQ-20..22, 29..38 | 5 |
|
||||
| (final review, no new REQs) | 6 |
|
||||
|
||||
> REQ-13 (`modules/README.md`) lands in phase 3 alongside the primitives.
|
||||
@@ -0,0 +1,101 @@
|
||||
# STATE — Nova Platform
|
||||
|
||||
> PDLC Phase 0 intake. Absolute ground truth as of 2026-08-28T19:09Z.
|
||||
> Single-pass discovery for the Product Owner/Manager.
|
||||
|
||||
---
|
||||
|
||||
## 1. Header
|
||||
|
||||
Project: nova-platform
|
||||
Initiative: PDLC Phase 0 — State Intake for next milestone planning
|
||||
Initiator: CIAgent (automated harness)
|
||||
Date (UTC): 2026-08-28T19:09:26Z
|
||||
Current Version: v1.0 milestone complete; release v0.1.6 on main; no active milestone in progress
|
||||
System Health: GREEN — 76/76 tests pass, engine boundary holds, zero OOS files, both happy paths green
|
||||
Raw Idea (≤ 3 sentences):
|
||||
Nova Platform v1.0 shipped a simplified infrastructure-delivery platform (contract → resolve → terraform adapter → apply) derived from the Nova/acdl reference, with security/audit/identity/CI-workflow machinery deliberately excluded.
|
||||
This intake was triggered by the PDLC system instruction to produce a STATE.md for the Product Owner to review before planning the next milestone.
|
||||
The desired outcome is a single-pass ground-truth snapshot enabling the PO to ask pointed Phase 1 questions without back-and-forth.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture State
|
||||
|
||||
Active Layers (which exist and are stable):
|
||||
[x] Core Primitives — 13 L1 modules: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds, kms-key, dynamodb, uptime
|
||||
[x] Domain Modules — 2 L2 patterns: microservice (vpc+ecs-cluster+ecs-service+iam-role+ecr+alb per D-038), static-assets (s3+cloudfront+kms-key per D-038)
|
||||
[x] API/Dev Surface — contract schema (array infrastructure D-015), stack schema (flat, no source D-037), environment schema (simplified D-017/D-018); resolver (core/contract_resolver.py), environment_check (core/environment_check.py), terraform adapter (adapters/terraform/adapter.py)
|
||||
[ ] UI/Agent Surface — N/A (no frontend, no agent UI; frontend-engineer persona deactivated per D-006)
|
||||
|
||||
Compute Topology (per environment):
|
||||
local: abstract (offline --check-only: contract → resolve → adapter → validate; no AWS, no terraform binary)
|
||||
dev: serverless (ECS Fargate + ALB + S3 + CloudFront via terraform apply; platform-managed VPC in terraform/platform/)
|
||||
staging: N/A — UNKNOWN — needs investigation (no qa.json environment file created; only dev.json exists)
|
||||
prod: N/A — UNKNOWN — needs investigation (no prod.json environment file created)
|
||||
dr: N/A — UNKNOWN — needs investigation (no dr.json environment file created)
|
||||
|
||||
Identity Stack in Force:
|
||||
auth: N/A — out of scope (Nova-idp, PAT lifecycle, ABAC evaluator explicitly OOS per D-007)
|
||||
token-vend: N/A — out of scope
|
||||
signing: N/A — out of scope (KMS signing, JWS attestation explicitly OOS)
|
||||
session: N/A — out of scope
|
||||
|
||||
Audit Stream:
|
||||
source of truth: N/A — out of scope (outbox writer, audit ledger, attestation matrix explicitly OOS per D-007)
|
||||
in-repo fallback: no
|
||||
retention policy: N/A
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Stack (concrete, not aspirational)
|
||||
|
||||
Language(s) and runtime(s): Python 3.11 (>=3.11 required per pyproject.toml); Bash (shell scripts); HCL (Terraform 1.9.* pinned)
|
||||
Build / packaging: pyproject.toml (setuptools backend); no CLI package (D-027 — scripts invoked via shell, no [project.scripts]); requirements-test.txt pins 5 deps
|
||||
CI / CD: Local shell only (scripts/run_ci.sh: lint → test → check-only); NO .github/workflows (OOS per D-007); NO pipelines/ central contract (OOS)
|
||||
Infrastructure: AWS (S3 state backend, DynamoDB lock table, ECS Fargate, ALB, CloudFront, WAF, RDS, KMS, ECR, IAM); terraform/bootstrap/ scripts create S3 bucket nova-tfstate-<account>-<region> + DynamoDB nova-tfstate-locks (D-022); only dev.json environment exists (account_id placeholder 000000000000 for offline)
|
||||
Data stores: S3 (state backend, static assets), DynamoDB (state locking — nova-tfstate-locks; also L1 dynamodb primitive for consumer tables), RDS (L1 rds primitive)
|
||||
Secrets / KMS: Static AWS key only (NOVA_AWS_* in .ciagent/.env.secrets, chmod 600, gitignored); NO OIDC (OOS per D-025); KMS via L1 kms-key primitive (consumer-side, not platform-side); NOVA_FORGE_TOKEN for gitea release (currently blank in .env.secrets — release pending)
|
||||
External integrations in scope: gitea @ https://git.cloudinit.dev/continuous-intelligence/nova-platform (release forge; token NOVA_FORGE_TOKEN); AWS (boto3 for bootstrap scripts + terraform apply)
|
||||
|
||||
---
|
||||
|
||||
## 4. Active Constraints (the load-bearing ones)
|
||||
|
||||
Locked Decisions: D-001 (milestone type major, first release), D-002 (all 13 L1 + 2 L2 in v1.0), D-003 (branching_strategy phase), D-004 (auto_commit/auto_push true), D-005 (test_first false), D-006 (personas: lead+data+backend active, frontend deactivated), D-007 (config.policy removed, ideation drops security), D-008 (secrets.scopes keeps forge + model-backend), D-009 (ship.confirm_before_ship true), D-010 (telemetry.persist true), D-011 (resolver modules_dir param, file reads permitted), D-012 (L2 opaque, flat stack, no children/wires expansion), D-013 (L2 interface.json + terraform_dir in registry), D-014 (interface.json: no nfrs, no intra_refs), D-015 (infrastructure is ARRAY, version optional), D-016 (interpolation ${env.*}/${contract.*} kept), D-017 (environment.schema.json kept simplified), D-018 (env fields: name/account_id/region/state_backend/network; no runner_role_arn/autonomy/confidence_threshold), D-019 (check() returns dict, raises EnvironmentNotFoundError), D-020 (run_platform.sh stages: check-only/plan-only/apply), D-021 (run_ci.sh glob py_compile), D-022 (lock table nova-tfstate-locks, NOT nova-outbox), D-023 (platform/main.tf = ONLY shared VPC), D-024 (ci-vpc = short-lived test VPC), D-025 (onboarding = IAM role cross-account assume, NOT OIDC, NOT user), D-026 (runner policy account parameterized, NOT hardcoded), D-027 (no CLI package, scripts only), D-028 (docs/modules/index links to modules/*/README.md), D-029 (drop STANDARDS.md, keep README-TEMPLATE.md trimmed), D-030 (moto pinned, minimal use), D-031 (run_platform.sh flags: --check-only/--plan-only/--quiet/--help only), D-032 (NOVA_* env prefix, NOVA_AWS_* → AWS_* copy then unset), D-033 (per-env contract variants differ ONLY in environment field), D-034 (engine-boundary test scans .py stripped of docstrings/comments), D-035 (index_document is L2 static-assets input, NOT L1 s3), D-036 (L2 children: microservice=vpc+ecs-cluster+ecs-service+iam-role+ecr+alb, static-assets=s3+cloudfront+kms-key), D-037 (adapter loads registry, no source in stack — grill C-1 fix), D-038 (L2 compositions locked, grill C-2 fix)
|
||||
|
||||
Active Invariants: No INV-* IDs formally registered (v1.0 used D-* decisions, not INV-* invariants). Load-bearing invariants: (1) engine-agnostic core — no aws_*/terraform/module "/provider "/resource " in .py outside adapters/terraform/ (verified by tests/test_engine_boundary.py); (2) structural conventions preserved with 9 locked deviations (D-012, D-013, D-015, D-017, D-018, D-019, D-022, D-023, D-025, D-027); (3) no OOS-creep — zero files from the PROJECT.md/REQUIREMENTS.md OOS list exist in the repo.
|
||||
|
||||
Standing Capability Gate: N/A — no GATE-* IDs registered
|
||||
|
||||
Anti-Goals Touched: PROJECT.md §"Out of scope" + REQUIREMENTS.md §"Out of scope (locked — do NOT implement in v1.0)": security/policy (kyverno, Wiz, Checkov, PolicyEngine), confidence signal + evidence outbox, identity/ABAC (Nova-idp, PAT, JWS, KMS signing, SoD, HITL, attestation, submission_readiness), CI/CD pipeline (.github/workflows, pipelines/, pipeline schemas), metrics/telemetry, leadership decks, decommission/env_transition/mode_resolver/onboarding flow beyond bootstrap, multi-project mode, OIDC federation.
|
||||
|
||||
Out-of-Scope (hard): kyverno-json adapter, Wiz adapter, Checkov custom rules, core/policy_engine.py, core/confidence_signal.py, core/outbox_writer.py, schemas/policy_check_result.schema.json, schemas/metrics_*.schema.json, audit ledger, attestation matrix, core/abac_evaluator.py, core/auth_store.py, core/jws_attestation.py, core/kms_signing.py, core/pat_lifecycle.py, core/separation_of_duties.py, core/hitl_gates.py, core/attestation_matrix.py, core/submission_readiness.py, .github/workflows/ci.yml, .github/workflows/deploy.yml, pipelines/, schemas/pipeline.schema.json, schemas/deploy-pipeline.schema.json, metrics/, core/metrics/, core/regression_verify*.py, modules/STANDARDS.md, PPTX/marp slides, core/env_transition.py, core/decommission_transform.py, core/mode_resolver.py, core/onboarding.py, OIDC federation.
|
||||
|
||||
---
|
||||
|
||||
## 5. Recent History & Quality Gates (last 1-2 milestones)
|
||||
|
||||
Last Shipped: v1.0 milestone — 2026-08-25 (tag v0.1.6 on main); delivered: simplified infrastructure-delivery platform with 13 L1 + 2 L2 modules, contract surface, resolver, terraform adapter, engine boundary, 5 terraform roots, bootstrap scripts, shell reproducibility, 76 tests, docs; 38 REQ-IDs all complete; 38 decisions (D-001..D-038)
|
||||
In Progress: No active milestone in progress (v1.0 complete; checkpoint cleared; next /ci-run starts a new milestone)
|
||||
Coverage Floor: UNKNOWN — needs investigation (pytest-cov not installed; no coverage measurement configured in pyproject.toml addopts; D-030 dropped pytest-cov as OOS)
|
||||
Recent Incidents: none
|
||||
Known Tensions: (1) NOVA_FORGE_TOKEN blank in .ciagent/.env.secrets — v0.1.6 shipped local-only (tag + merge complete, gitea release pending); the forge repo was created during this intake but the token must be set before releases can be created. (2) Only dev.json environment exists — qa/prod/dr environment files were not created in v1.0 (contracts reference them via enum but environment_check will raise EnvironmentNotFoundError for qa/prod/dr). (3) Coverage measurement is absent — pytest-cov was dropped as OOS but this means no coverage floor is enforceable.
|
||||
|
||||
---
|
||||
|
||||
## 6. Agent Context & Assumptions (Agent Initiators Only)
|
||||
|
||||
Missing Context: (1) qa/prod/dr environment JSON files — not created in v1.0 (only core/environments/dev.json exists); the contract schema allows these environment values but no environment record exists for them. (2) Coverage percentage — pytest-cov is not installed; no coverage data available. (3) NORTH_STAR.md — not created (the reference acdl has one; nova-platform deferred it to a future /ci-run specify stage). (4) Live AWS verification — bootstrap scripts + terraform roots were authored but never applied against real AWS (offline-only verification via --check-only).
|
||||
|
||||
Agent Assumptions: (1) Assumed the gitea forge token from the acdl reference repo (/home/opencode/acdl/.env.secrets) is valid for the nova-platform org — verified via API (200 auth). (2) Assumed the next milestone is v1.1 (incrementing from v1.0) — no ROADMAP.md entry exists for a next milestone yet. (3) Assumed PDLC Phase 0 STATE.md work belongs on a new milestone/phase-0 branch hierarchy (milestone/v1.1-pdlc-state → phase/00-pre-execution) per the branch gate convention. (4) Assumed "merge to the forge upstream but DO NOT perform a release" means: push main + branches to origin, do NOT create a gitea release (no tag push, no release API call).
|
||||
|
||||
---
|
||||
|
||||
## 7. Canonical State References (Version/Hash)
|
||||
|
||||
Vision/Strategy doc: UNKNOWN — needs investigation (NORTH_STAR.md not created; PROJECT.md §"Vision / Core Value" serves as the de facto vision but is not versioned as a strategy doc)
|
||||
Architecture document: v0.1.6 (tag on main commit fe9ab7e); .ciagent/ARCHITECTURE.md + docs/architecture.md are the architecture source of truth
|
||||
Last approved SPEC: N/A — no SPEC-ID system in use; REQUIREMENTS.md (38 REQ-IDs, REQ-01..REQ-38) is the requirements source of truth at commit fe9ab7e
|
||||
Decision log: fe9ab7e (last synced commit on main); decisions D-001..D-035 in .ciagent/CLARIFY.md; D-036..D-038 (grill fixes) in .ciagent/CLARIFY.md §"Grill fixes"; D-001..D-010 also in .ciagent/PROJECT.md
|
||||
Invariants catalog: N/A — no INV-* IDs formally registered; load-bearing invariants documented in STATE.md §4 above
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"active_project": "nova-platform",
|
||||
"active_milestone": "v1.0",
|
||||
"autonomy": {
|
||||
"level": "supervised",
|
||||
"escalation_hooks": [
|
||||
"deploy",
|
||||
"delete_data",
|
||||
"merge_to_main",
|
||||
"verification_failure",
|
||||
"ship"
|
||||
],
|
||||
"clarify_budget": 10,
|
||||
"decision_confidence_threshold": 0.75,
|
||||
"max_revision_iterations": 2,
|
||||
"max_verification_retries": 2,
|
||||
"escalation_timeout_ms": 300000
|
||||
},
|
||||
"model_profile": "quality",
|
||||
"parallelization": {
|
||||
"enabled": true,
|
||||
"max_concurrent_agents": 5,
|
||||
"min_plans_for_parallel": 2,
|
||||
"max_concurrent_projects": 3
|
||||
},
|
||||
"verification": {
|
||||
"automated_only": true,
|
||||
"escalate_visual": true,
|
||||
"escalate_external_integration": true,
|
||||
"test_first": false
|
||||
},
|
||||
"security": {
|
||||
"auto_accept_low_severity": true,
|
||||
"auto_mitigate_medium_severity": true,
|
||||
"escalate_high_severity": true,
|
||||
"bash_allowlist": {
|
||||
"allowed_commands": [
|
||||
"git",
|
||||
"ls",
|
||||
"cat",
|
||||
"head",
|
||||
"tail",
|
||||
"wc",
|
||||
"echo",
|
||||
"mkdir",
|
||||
"cp",
|
||||
"mv",
|
||||
"rm",
|
||||
"touch",
|
||||
"pwd",
|
||||
"which",
|
||||
"env",
|
||||
"printenv",
|
||||
"python3",
|
||||
"pytest",
|
||||
"pip",
|
||||
"terraform",
|
||||
"curl",
|
||||
"wget",
|
||||
"docker",
|
||||
"docker-compose"
|
||||
],
|
||||
"max_output_bytes": 1048576,
|
||||
"timeout_ms": 30000,
|
||||
"blocked_env_vars": [
|
||||
"HOME",
|
||||
"PATH",
|
||||
"USER",
|
||||
"SHELL",
|
||||
"AWS_*",
|
||||
"*_TOKEN",
|
||||
"*_KEY",
|
||||
"*_SECRET",
|
||||
"*_PASSWORD",
|
||||
"*_CREDENTIAL",
|
||||
"GITHUB_TOKEN",
|
||||
"GITHUB_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OLLAMA_CLOUD_API_KEY",
|
||||
"NOVA_FORGE_TOKEN"
|
||||
]
|
||||
}
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "phase",
|
||||
"auto_commit": true,
|
||||
"auto_push": true
|
||||
},
|
||||
"secrets": {
|
||||
"sources": [
|
||||
".env",
|
||||
".env.secrets",
|
||||
".env.*"
|
||||
],
|
||||
"disallow": [
|
||||
"shell_env",
|
||||
"netrc",
|
||||
"keychain",
|
||||
"rc_files",
|
||||
"global_config"
|
||||
],
|
||||
"scopes": {
|
||||
"forge": "NOVA_FORGE_TOKEN",
|
||||
"gitea": "NOVA_FORGE_TOKEN",
|
||||
"github": "GITHUB_TOKEN",
|
||||
"gitlab": "GITLAB_TOKEN",
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"ollama_cloud": "OLLAMA_CLOUD_API_KEY"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"forge": "gitea",
|
||||
"gitea": {
|
||||
"base_url": "https://git.cloudinit.dev",
|
||||
"owner": "continuous-intelligence",
|
||||
"repo": "nova-platform",
|
||||
"token_scope": "gitea"
|
||||
},
|
||||
"github": {
|
||||
"owner": "",
|
||||
"repo": "",
|
||||
"token_scope": "github"
|
||||
},
|
||||
"gitlab": {
|
||||
"base_url": "",
|
||||
"owner": "",
|
||||
"repo": "",
|
||||
"token_scope": "gitlab"
|
||||
}
|
||||
},
|
||||
"ship": {
|
||||
"per_phase": true,
|
||||
"require_release": true,
|
||||
"allow_skip": false,
|
||||
"confirm_before_ship": true,
|
||||
"max_release_retries": 3,
|
||||
"release_blocking": false
|
||||
},
|
||||
"backend": {
|
||||
"provider": "auto",
|
||||
"agent_backends": {
|
||||
"opencode": {
|
||||
"enabled": true
|
||||
},
|
||||
"codex": {
|
||||
"enabled": true
|
||||
},
|
||||
"claude-code": {
|
||||
"enabled": true
|
||||
},
|
||||
"hermes": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"llm_backends": {
|
||||
"openai": {
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"api_key_env": "OPENAI_API_KEY",
|
||||
"model": "gpt-4o",
|
||||
"model_profile": "quality",
|
||||
"timeout_ms": 60000
|
||||
},
|
||||
"ollama-local": {
|
||||
"base_url": "http://localhost:11434",
|
||||
"model_profile": "balanced"
|
||||
},
|
||||
"ollama-cloud": {
|
||||
"base_url": "",
|
||||
"api_key_env": "OLLAMA_CLOUD_API_KEY",
|
||||
"model_profile": "quality",
|
||||
"timeout_ms": 60000
|
||||
},
|
||||
"anthropic": {
|
||||
"base_url": "https://api.anthropic.com",
|
||||
"api_key_env": "ANTHROPIC_API_KEY",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"api_version": "2023-06-01",
|
||||
"model_profile": "quality",
|
||||
"timeout_ms": 60000
|
||||
}
|
||||
}
|
||||
},
|
||||
"ideation": {
|
||||
"enabled": true,
|
||||
"categories": [
|
||||
"quality",
|
||||
"architecture",
|
||||
"coverage",
|
||||
"improvement"
|
||||
],
|
||||
"confidence_threshold": 0.6,
|
||||
"max_ideas": 20,
|
||||
"external_signals": {
|
||||
"npm_audit": true,
|
||||
"osv_advisories": true,
|
||||
"dependency_staleness": true
|
||||
},
|
||||
"cross_project": {
|
||||
"enabled": false,
|
||||
"similarity_weight": 0.5
|
||||
},
|
||||
"chaos": {
|
||||
"enabled": true,
|
||||
"scenarios": [
|
||||
"backend_unavailable",
|
||||
"requirement_change",
|
||||
"test_coverage_drop"
|
||||
]
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"max_concurrent_sessions": 3,
|
||||
"session_timeout_ms": 3600000,
|
||||
"session_isolation": "branch"
|
||||
},
|
||||
"personas": {
|
||||
"enabled": true,
|
||||
"territory_enforcement": "warn",
|
||||
"personas": [
|
||||
{
|
||||
"name": "lead-developer",
|
||||
"domain": "coordination",
|
||||
"frameworks": [],
|
||||
"constraints": [
|
||||
"pragmatic",
|
||||
"battle-tested defaults"
|
||||
],
|
||||
"territory": []
|
||||
},
|
||||
{
|
||||
"name": "data-engineer",
|
||||
"domain": "data",
|
||||
"frameworks": [
|
||||
"terraform"
|
||||
],
|
||||
"constraints": [
|
||||
"schema-first",
|
||||
"type-safe",
|
||||
"migration-driven"
|
||||
],
|
||||
"territory": [
|
||||
"**/terraform/**",
|
||||
"**/modules/**",
|
||||
"**/schemas/**",
|
||||
"**/*.tf",
|
||||
"**/*.tf.json",
|
||||
"**/*.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "backend-engineer",
|
||||
"domain": "backend",
|
||||
"frameworks": [
|
||||
"python",
|
||||
"fastapi"
|
||||
],
|
||||
"constraints": [
|
||||
"api-first",
|
||||
"strict-typing",
|
||||
"dependency-injection"
|
||||
],
|
||||
"territory": [
|
||||
"**/core/**",
|
||||
"**/scripts/**",
|
||||
"**/adapters/**",
|
||||
"**/contracts/**",
|
||||
"**/tests/**",
|
||||
"**/*.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "frontend-engineer",
|
||||
"domain": "frontend",
|
||||
"active": false,
|
||||
"frameworks": [
|
||||
"react",
|
||||
"next.js"
|
||||
],
|
||||
"constraints": [
|
||||
"component-first",
|
||||
"server-components",
|
||||
"minimal-client-js"
|
||||
],
|
||||
"territory": [
|
||||
"**/components/**",
|
||||
"**/pages/**",
|
||||
"**/hooks/**",
|
||||
"**/styles/**",
|
||||
"**/*.tsx",
|
||||
"**/*.css",
|
||||
"**/*.vue"
|
||||
],
|
||||
"reason": "Nova Platform has no frontend. Deactivated."
|
||||
}
|
||||
]
|
||||
},
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"format": "json",
|
||||
"file": ".ciagent/logs/ciagent.jsonl"
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"persist": true
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
.env.*
|
||||
state.json
|
||||
audit.json
|
||||
*.tmp
|
||||
.DS_Store
|
||||
runner-data/
|
||||
.env.secrets
|
||||
terraform/bootstrap/.bootstrap_state.json
|
||||
|
||||
# CIAgent runtime artifacts
|
||||
.ciagent/logs/
|
||||
|
||||
# Terraform — recursively ignore .terraform dirs, lock files, plans, and state
|
||||
**/.terraform/
|
||||
**/.terraform.lock.hcl
|
||||
**/tfplan
|
||||
**/*.tfstate*
|
||||
|
||||
# Credential patterns
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.cer
|
||||
*.crt
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
.coverage
|
||||
.venv/
|
||||
nova_platform.egg-info/
|
||||
@@ -0,0 +1,127 @@
|
||||
# Nova Platform
|
||||
|
||||
> Nova Platform — infrastructure delivery, simplified. A consumer declares
|
||||
> intent in a YAML contract; the platform resolves it to a stack, compiles
|
||||
> it through the Terraform adapter, and applies it. Every deployment is
|
||||
> reproducible from the shell, not just in CI.
|
||||
|
||||
Nova Platform is the **infrastructure-delivery core** of the Nova model.
|
||||
The DevSecOps, identity, audit-ledger, and central CI-pipeline-contract
|
||||
machinery of the reference are intentionally removed. What remains: a
|
||||
consumer writes a small YAML contract that names one or more modules by
|
||||
name + version, selects an environment, and supplies module-specific
|
||||
inputs. The platform resolves the contract to a stack instance, compiles
|
||||
it through the Terraform adapter, and applies it.
|
||||
|
||||
- **Consumer guide:** [`docs/consumer-guide.md`](docs/consumer-guide.md)
|
||||
- **Contracts:** [`docs/contracts/`](docs/contracts/index.md)
|
||||
- **Environments:** [`docs/environments/`](docs/environments/index.md)
|
||||
- **Architecture:** [`docs/architecture.md`](docs/architecture.md)
|
||||
- **Modules:** [`docs/modules/`](docs/modules/index.md)
|
||||
|
||||
## How to run
|
||||
|
||||
### Quick start (offline, no AWS required)
|
||||
|
||||
The fastest way to verify the platform works — no AWS credentials, no
|
||||
bootstrap, no cost.
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install -r requirements-test.txt
|
||||
|
||||
# 1. Run the test suite (all offline)
|
||||
python3 -m pytest tests/ -q
|
||||
|
||||
# 2. Run the platform in check-only mode (offline — contract -> resolve ->
|
||||
# adapter -> structure validation). Uses the default sample contract.
|
||||
bash scripts/run_platform.sh --check-only contracts/static-assets.yml
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
|
||||
# 3. Reproduce the full CI pipeline locally (lint -> test -> check-only)
|
||||
bash scripts/run_ci.sh
|
||||
# Expected: "=== CI PIPELINE OK ==="
|
||||
|
||||
# Show all run_platform.sh flags:
|
||||
bash scripts/run_platform.sh --help
|
||||
```
|
||||
|
||||
### Run against live AWS (requires credentials + bootstrap)
|
||||
|
||||
> Prerequisites: a platform-managed environment (see
|
||||
> [docs/environments/](docs/environments/index.md); `core/environments/dev.json`
|
||||
> is the sample), AWS credentials for dev (in `.ciagent/.env.secrets`,
|
||||
> gitignored; see [Credentials](#credentials)), `terraform` (pin `>= 1.9, < 1.10`),
|
||||
> `python3` + `boto3` + `jsonschema` + `pyyaml`.
|
||||
|
||||
```bash
|
||||
# 1. Bootstrap the AWS state backend + runner IAM user (one-time, idempotent)
|
||||
# See terraform/bootstrap/README.md for the full runbook.
|
||||
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
|
||||
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
|
||||
export AWS_DEFAULT_REGION="us-east-1"
|
||||
python3 terraform/bootstrap/create_state_backend.py
|
||||
python3 terraform/bootstrap/create_iam_user.py # prints the initial key
|
||||
bash scripts/rotate_spike_key.sh # writes .ciagent/.env.secrets
|
||||
|
||||
# 2. Run the full platform pipeline (contract -> environment check -> stack ->
|
||||
# adapter -> terraform init/validate/plan -> apply).
|
||||
bash scripts/run_platform.sh contracts/microservice.yml
|
||||
# Expected: "=== PLATFORM APPLY OK ==="
|
||||
|
||||
# Or plan-only (contract -> stack -> adapter -> terraform plan; no apply):
|
||||
bash scripts/run_platform.sh --plan-only contracts/static-assets.yml
|
||||
|
||||
# Add --quiet to suppress streaming (output to log files only):
|
||||
bash scripts/run_platform.sh --quiet contracts/static-assets.yml
|
||||
```
|
||||
|
||||
### run_platform.sh flags (D-031)
|
||||
|
||||
| Flag | Mode | AWS required | Description |
|
||||
|------|------|--------------|-------------|
|
||||
| `--check-only` | offline | no | contract → resolve → adapter → structure validation |
|
||||
| `--plan-only` | AWS | yes | above + `terraform init`/`validate`/`plan` (no apply) |
|
||||
| `--quiet` | any | — | suppress streaming output |
|
||||
| `-h`, `--help` | — | — | show usage |
|
||||
| *(none)* | apply | yes | full path: above + `terraform apply -auto-approve` |
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Purpose | Status |
|
||||
|------|---------|--------|
|
||||
| `core/` | Platform code: contract resolver, environment check, environments | active |
|
||||
| `schemas/` | JSON Schemas (draft 2020-12): contract, stack, environment | active |
|
||||
| `adapters/terraform/` | The Terraform adapter — the only engine-specific code | active |
|
||||
| `terraform/` | State backend (S3 + DynamoDB) + bootstrap scripts + platform/onboarding/ci-vpc | active |
|
||||
| `modules/` | L1 primitives (13) + L2 patterns (2) + `registry.json`. Each module has `interface.json` + `terraform/` | active |
|
||||
| `contracts/` | Sample consumer contracts (`static-assets.yml`, `microservice.yml`) + per-env variants | active |
|
||||
| `scripts/` | `run_platform.sh` (pipeline runner), `run_ci.sh` (local CI mirror), `rotate_spike_key.sh` | active |
|
||||
| `tests/` | Pytest suite (all offline — resolver, adapter, schemas, engine boundary, environment check) | active |
|
||||
| `docs/` | Documentation: consumer guide, contracts, environments, architecture, modules | active |
|
||||
| `.ciagent/` | CIAgent config + locked decisions (`ARCHITECTURE.md`, `PROJECT.md`, `CLARIFY.md`) | active |
|
||||
|
||||
## Credentials
|
||||
|
||||
Nova Platform uses a **static AWS key** for dev/local operation. There is no
|
||||
zero-trust federation layer in v1.0 — that is out of scope.
|
||||
|
||||
- The runner key is stored in **`.ciagent/.env.secrets`** (gitignored,
|
||||
`chmod 600`) using the `NOVA_AWS_*` prefix (D-032):
|
||||
`NOVA_AWS_ACCESS_KEY_ID`, `NOVA_AWS_SECRET_ACCESS_KEY`.
|
||||
- `scripts/run_platform.sh` copies `NOVA_AWS_*` to the standard `AWS_*`
|
||||
env vars before invoking Terraform, then unsets the `NOVA_*` copies.
|
||||
- Bootstrap uses a one-shot root key via `NOVA_BOOTSTRAP_AWS_*` env vars
|
||||
(never committed, never echoed). See
|
||||
[`terraform/bootstrap/README.md`](terraform/bootstrap/README.md) for the
|
||||
full bootstrap runbook (state backend + runner IAM user + key rotation).
|
||||
- Onboarding creates a per-consumer IAM **role** (cross-account assume-role
|
||||
pattern, D-025) — not a user, not federation. See
|
||||
`terraform/onboarding/main.tf`.
|
||||
|
||||
## Consumer guide
|
||||
|
||||
A step-by-step guide for a consumer to create a repo, write a contract,
|
||||
validate it offline, and run it against AWS is at
|
||||
[`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic
|
||||
across all modules; `static-assets` is the worked example.
|
||||
@@ -0,0 +1,3 @@
|
||||
from adapters.terraform.adapter import adapt
|
||||
|
||||
__all__ = ["adapt"]
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Nova Platform — Terraform Adapter.
|
||||
|
||||
The ONLY engine-specific code in the platform (per REQ-09, verified by
|
||||
tests/test_engine_boundary.py). Loads modules/registry.json internally
|
||||
to map module -> terraform_dir (per D-037/C-1 grill fix — the resolver
|
||||
does NOT put a `source` field in the stack; the adapter resolves it
|
||||
here, inside the engine boundary).
|
||||
|
||||
Stateless assembler: no `terraform` CLI invocation, no state files, no
|
||||
plan files. Emits Terraform HCL: one `module "x" { source = ...; <inputs> }`
|
||||
block per stack resource.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_registry(repo_root):
|
||||
"""Load modules/registry.json -> {module_name: terraform_dir}."""
|
||||
registry_path = os.path.join(str(repo_root), "modules", "registry.json")
|
||||
with open(registry_path) as fh:
|
||||
registry = json.load(fh)
|
||||
return {name: list(versions.values())[0].get("terraform_dir")
|
||||
for name, versions in registry.items()
|
||||
if list(versions.values())[0].get("terraform_dir")}
|
||||
|
||||
|
||||
def _tf_value(value):
|
||||
"""Render a Python value as an HCL expression."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
if isinstance(value, list):
|
||||
return "[" + ", ".join(_tf_value(v) for v in value) + "]"
|
||||
if isinstance(value, dict):
|
||||
return "{ " + ", ".join(f"{k} = {_tf_value(v)}" for k, v in value.items()) + " }"
|
||||
return json.dumps(str(value))
|
||||
|
||||
|
||||
def _emit_module_block(resource, terraform_dirs, repo_root):
|
||||
"""Emit one `module "x" { source = ...; <inputs> }` block."""
|
||||
module_name = resource["module"]
|
||||
rid = module_name.replace("-", "_")
|
||||
tf_dir = terraform_dirs.get(module_name)
|
||||
if tf_dir is None:
|
||||
raise ValueError(f"module '{module_name}' has no terraform_dir in registry")
|
||||
source = os.path.join(str(repo_root), tf_dir)
|
||||
lines = [f'module "{rid}" {{', f' source = "{source}"']
|
||||
for key, val in resource.get("inputs", {}).items():
|
||||
if key == "region":
|
||||
continue
|
||||
lines.append(f" {key} = {_tf_value(val)}")
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def adapt(stack, repo_root):
|
||||
"""Compile a flat stack dict to Terraform HCL.
|
||||
|
||||
Args:
|
||||
stack: a flat stack dict conforming to schemas/stack.schema.json
|
||||
(NO `source` field per D-037 — the adapter resolves
|
||||
module -> terraform_dir via the registry).
|
||||
repo_root: Path to the repo root (the adapter loads
|
||||
modules/registry.json from here).
|
||||
|
||||
Returns:
|
||||
A string of Terraform HCL with one `module "x" {}` block per
|
||||
stack resource.
|
||||
"""
|
||||
terraform_dirs = _load_registry(repo_root)
|
||||
blocks = []
|
||||
for resource in stack.get("resources", []):
|
||||
blocks.append(_emit_module_block(resource, terraform_dirs, repo_root))
|
||||
return "\n\n".join(blocks) + "\n"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
argv = argv or sys.argv[1:]
|
||||
if len(argv) < 1:
|
||||
print("usage: adapter.py <stack.json> [out.tf]", file=sys.stderr)
|
||||
return 2
|
||||
stack_path = argv[0]
|
||||
out_path = argv[1] if len(argv) > 1 else None
|
||||
repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
with open(stack_path) as fh:
|
||||
stack = json.load(fh)
|
||||
hcl = adapt(stack, repo_root)
|
||||
if out_path:
|
||||
with open(out_path, "w") as fh:
|
||||
fh.write(hcl)
|
||||
else:
|
||||
print(hcl)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,9 @@
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
@@ -0,0 +1,9 @@
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: dr
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
@@ -0,0 +1,9 @@
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: prod
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
@@ -0,0 +1,9 @@
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: qa
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
@@ -0,0 +1,9 @@
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
@@ -0,0 +1,9 @@
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
@@ -0,0 +1,9 @@
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: dr
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
@@ -0,0 +1,9 @@
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: prod
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
@@ -0,0 +1,9 @@
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: qa
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
@@ -0,0 +1,9 @@
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Nova Platform — Contract Resolver.
|
||||
|
||||
Resolves a validated consumer contract to a Stack instance (a flat dict
|
||||
conforming to schemas/stack.schema.json).
|
||||
|
||||
Flow:
|
||||
1. Validate the contract dict against schemas/contract.schema.json.
|
||||
2. Load the environment via core.environment_check.check().
|
||||
3. Build an interpolation context {'env': env, 'contract': contract}.
|
||||
4. For each infrastructure entry: look up the module + version in the
|
||||
registry, interpolate ${env.*} / ${contract.*} tokens in inputs,
|
||||
and emit a flat stack resource {module, version, inputs}.
|
||||
5. Return the stack dict.
|
||||
|
||||
Engine-agnostic: no aws_*, no Terraform terms, no module paths. The stack
|
||||
carries NO 'source' field (D-037/C-1 grill fix) — the adapter loads the
|
||||
registry to map module -> terraform_dir. L2 is opaque (D-012): a single
|
||||
stack resource, no children/wires expansion.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
_TOKEN_RE = re.compile(r"\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}")
|
||||
|
||||
|
||||
class ModuleNotFoundError(KeyError):
|
||||
"""Raised when a contract references a module not in the registry."""
|
||||
|
||||
|
||||
class VersionNotFoundError(KeyError):
|
||||
"""Raised when a contract references a version not in the registry."""
|
||||
|
||||
|
||||
def _lookup_dotted(context, dotted):
|
||||
parts = dotted.split(".")
|
||||
cur = context
|
||||
for part in parts:
|
||||
if isinstance(cur, dict) and part in cur:
|
||||
cur = cur[part]
|
||||
else:
|
||||
raise KeyError(dotted)
|
||||
return cur
|
||||
|
||||
|
||||
def _expand_vars(value, context):
|
||||
if isinstance(value, str):
|
||||
def _replace(match):
|
||||
token = match.group(1)
|
||||
try:
|
||||
resolved = _lookup_dotted(context, token)
|
||||
except KeyError:
|
||||
raise ValueError(f"unresolved interpolation token: ${{{token}}}")
|
||||
if isinstance(resolved, (dict, list)):
|
||||
return json.dumps(resolved)
|
||||
return str(resolved)
|
||||
return _TOKEN_RE.sub(_replace, value)
|
||||
if isinstance(value, dict):
|
||||
return {k: _expand_vars(v, context) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_expand_vars(v, context) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _latest_version(registry, module_name):
|
||||
versions = registry[module_name]
|
||||
non_deprecated = [(v, e) for v, e in versions.items()
|
||||
if not e.get("deprecated", False)]
|
||||
if not non_deprecated:
|
||||
non_deprecated = list(versions.items())
|
||||
non_deprecated.sort(key=lambda x: [int(p) for p in x[0].split(".")],
|
||||
reverse=True)
|
||||
return non_deprecated[0][0]
|
||||
|
||||
|
||||
def _load_schema(path):
|
||||
with open(path) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def resolve(contract, registry, modules_dir, environments_dir=None,
|
||||
repo_root=None):
|
||||
"""Resolve a validated contract dict to a flat Stack dict.
|
||||
|
||||
Args:
|
||||
contract: validated contract dict (must conform to
|
||||
schemas/contract.schema.json).
|
||||
registry: modules/registry.json loaded as a dict.
|
||||
modules_dir: Path to the modules/ directory (unused for L2-opaque
|
||||
resolution but kept per D-011 for future interface.json reads).
|
||||
environments_dir: Path to core/environments/. If None, derived from
|
||||
repo_root / 'core' / 'environments'.
|
||||
repo_root: Path to the repo root. If None, derived from modules_dir
|
||||
parent's parent (modules_dir is <root>/modules).
|
||||
|
||||
Returns:
|
||||
A flat stack dict conforming to schemas/stack.schema.json:
|
||||
{contract_id, contract_name, environment, resources: [{module,
|
||||
version, inputs}]}.
|
||||
|
||||
Raises:
|
||||
ModuleNotFoundError: contract references an unknown module.
|
||||
VersionNotFoundError: contract references an unknown version.
|
||||
jsonschema.ValidationError: contract does not conform to schema.
|
||||
ValueError: unresolved interpolation token.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = Path(modules_dir).parent.parent
|
||||
if environments_dir is None:
|
||||
environments_dir = Path(repo_root) / "core" / "environments"
|
||||
|
||||
contract_schema_path = Path(repo_root) / "schemas" / "contract.schema.json"
|
||||
contract_schema = _load_schema(contract_schema_path)
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
from core import environment_check
|
||||
env = environment_check.check(contract["environment"], environments_dir)
|
||||
# Expose 'environment' as an alias for the env's 'name' field so
|
||||
# ${env.environment} resolves (the env JSON uses 'name', but contracts
|
||||
# reference the environment by ${env.environment}).
|
||||
env["environment"] = env.get("name", contract["environment"])
|
||||
|
||||
context = {"env": env, "contract": contract}
|
||||
|
||||
resources = []
|
||||
for item in contract["infrastructure"]:
|
||||
module_name = item["module"]
|
||||
if module_name not in registry:
|
||||
raise ModuleNotFoundError(module_name)
|
||||
version = item.get("version")
|
||||
if version is None:
|
||||
version = _latest_version(registry, module_name)
|
||||
elif version not in registry[module_name]:
|
||||
raise VersionNotFoundError(f"{module_name}@{version}")
|
||||
inputs = _expand_vars(item.get("inputs", {}), context)
|
||||
resources.append({
|
||||
"module": module_name,
|
||||
"version": version,
|
||||
"inputs": inputs,
|
||||
})
|
||||
|
||||
return {
|
||||
"contract_id": contract["id"],
|
||||
"contract_name": contract["name"],
|
||||
"environment": contract["environment"],
|
||||
"resources": resources,
|
||||
}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
argv = argv or sys.argv[1:]
|
||||
if len(argv) < 2:
|
||||
print("usage: contract_resolver.py <contract.yaml> [out.json]",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
contract_path = argv[0]
|
||||
out_path = argv[1] if len(argv) > 1 else None
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
with open(contract_path) as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
with open(repo_root / "modules" / "registry.json") as fh:
|
||||
registry = json.load(fh)
|
||||
stack = resolve(contract, registry, repo_root / "modules",
|
||||
repo_root=repo_root)
|
||||
if out_path:
|
||||
with open(out_path, "w") as fh:
|
||||
json.dump(stack, fh, indent=2)
|
||||
else:
|
||||
print(json.dumps(stack, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Nova Platform — Environment Check.
|
||||
|
||||
Loads and validates a platform-managed environment JSON file.
|
||||
|
||||
Simplified per D-019: check(env_name, environments_dir) -> dict, raises
|
||||
EnvironmentNotFoundError on missing env. Drops the reference's
|
||||
(ok, message) tuple, _onboarding_message, and main() CLI.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class EnvironmentNotFoundError(FileNotFoundError):
|
||||
"""Raised when a named environment has no JSON file."""
|
||||
|
||||
|
||||
def check(env_name, environments_dir):
|
||||
"""Load and return the environment dict for env_name.
|
||||
|
||||
Args:
|
||||
env_name: environment name (dev, qa, prod, dr).
|
||||
environments_dir: Path to the core/environments/ directory.
|
||||
|
||||
Returns:
|
||||
The parsed environment dict.
|
||||
|
||||
Raises:
|
||||
EnvironmentNotFoundError: no <env_name>.json in environments_dir.
|
||||
"""
|
||||
env_path = Path(environments_dir) / f"{env_name}.json"
|
||||
if not env_path.exists():
|
||||
raise EnvironmentNotFoundError(
|
||||
f"environment '{env_name}' not found at {env_path}")
|
||||
with open(env_path) as fh:
|
||||
env = json.load(fh)
|
||||
return env
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "dev",
|
||||
"description": "Sample dev environment for offline/local testing. account_id placeholder (000000000000) for offline mode.",
|
||||
"account_id": "000000000000",
|
||||
"region": "us-east-1",
|
||||
"state_backend": {
|
||||
"bucket": "nova-tfstate-dev-us-east-1",
|
||||
"lock_table": "nova-tfstate-locks"
|
||||
},
|
||||
"network": {
|
||||
"vpc_cidr": "10.0.0.0/16",
|
||||
"azs": ["us-east-1a", "us-east-1b"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
# Architecture
|
||||
|
||||
> **Status:** v1.0 (current). This document mirrors `.ciagent/ARCHITECTURE.md`.
|
||||
> Where the two conflict, `.ciagent/ARCHITECTURE.md` wins.
|
||||
|
||||
## 0. Purpose
|
||||
|
||||
Nova Platform is the **infrastructure-delivery core**. A consumer declares
|
||||
intent in a YAML contract; the platform resolves it to a stack, compiles it
|
||||
through the Terraform adapter, and applies it. The DevSecOps, identity,
|
||||
audit-ledger, and central CI-pipeline-contract machinery of the Nova
|
||||
reference are intentionally removed — see the OOS list below.
|
||||
|
||||
## 1. Layers (4)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ 1. Contract Surface schemas/contract.schema.json
|
||||
│ contracts/*.yml (samples)
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 2. Resolution core/contract_resolver.py
|
||||
│ core/environment_check.py
|
||||
│ schemas/stack.schema.json
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 3. Engine Adapter adapters/terraform/ (the only
|
||||
│ (only engine-specific) engine-specific code)
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ 4. Apply terraform/ (bootstrap, modules)
|
||||
│ scripts/run_platform.sh
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer 1 — Contract Surface
|
||||
|
||||
A consumer writes a small YAML contract. The `infrastructure` field is an
|
||||
**array** (D-015), not a map:
|
||||
|
||||
```yaml
|
||||
id: stsi
|
||||
name: My Static Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: my-static-site-assets
|
||||
index_document: index.html
|
||||
```
|
||||
|
||||
Validated against `schemas/contract.schema.json`. The contract is the only
|
||||
consumer-facing surface. It is engine-agnostic — no `aws_*` terms.
|
||||
|
||||
### Layer 2 — Resolution
|
||||
|
||||
`core/contract_resolver.py` resolves a validated contract to a Stack
|
||||
instance (a typed structure conforming to `schemas/stack.schema.json`).
|
||||
Resolution is pure: contract in, stack out. No I/O beyond local file reads
|
||||
for module metadata (D-011). No engine terms.
|
||||
|
||||
`core/environment_check.py` validates that the named environment exists in
|
||||
`core/environments/*.json` and returns its definition. Environments are
|
||||
platform-managed (consumers provide no AWS account, VPC, or state bucket).
|
||||
|
||||
Interpolation (D-016): the resolver expands `${env.<field>}` and
|
||||
`${contract.<field>}` tokens after the environment is loaded. Unknown tokens
|
||||
raise `ValueError`.
|
||||
|
||||
### Layer 3 — Engine Adapter
|
||||
|
||||
`adapters/terraform/` is the only engine-specific code. It takes a Stack
|
||||
and emits Terraform (`module "x" { source = "../../modules/..." }` blocks).
|
||||
The adapter is a stateless assembler — lifecycle ownership belongs to
|
||||
Terraform via the shell orchestrator. This is the only place `aws_*` /
|
||||
Terraform terms appear.
|
||||
|
||||
### Layer 4 — Apply
|
||||
|
||||
`scripts/run_platform.sh` orchestrates: contract → resolve → adapter →
|
||||
`terraform init` → `terraform plan` → `terraform apply`. Modes (D-031):
|
||||
`--check-only` (offline, structure validation), `--plan-only` (no apply),
|
||||
full (apply). `--quiet` suppresses streaming.
|
||||
|
||||
## 2. Engine Boundary (Enforced)
|
||||
|
||||
The engine boundary is strict. Code outside `adapters/terraform/` MUST NOT
|
||||
contain engine-specific terms (`aws_s3_bucket`, `aws_*`, Terraform HCL).
|
||||
This invariant is verified by tests (`tests/test_engine_boundary.py`,
|
||||
scope per D-034: `.py` files in `core/`, `schemas/`, `contracts/`,
|
||||
`tests/`, `scripts/`, root — excluding `adapters/terraform/`, `modules/`,
|
||||
`.tf`/`.md`/`.json` data files).
|
||||
|
||||
## 3. What is NOT here (intentionally removed vs the reference)
|
||||
|
||||
Nova Platform is a simplified, infrastructure-only platform. The following
|
||||
reference features are **out of scope** for v1.0:
|
||||
|
||||
- No confidence signal — no score gating apply.
|
||||
- No audit outbox — no hash-chained evidence events.
|
||||
- No policy engine / policy adapter — no policy checks.
|
||||
- No identity layer, no attribute-based authorization, no human-in-the-loop
|
||||
approval gates.
|
||||
- No reusable CI workflow — local shell only (`scripts/run_ci.sh`).
|
||||
- No central pipeline contract — no `pipelines/` directory.
|
||||
- No platform telemetry / metrics.
|
||||
- No decommission transform, env-transition transform, or onboarding flow
|
||||
beyond bootstrap.
|
||||
- No leadership decks or slide rendering.
|
||||
|
||||
## 4. Module Catalog
|
||||
|
||||
L1 primitives (single resources) + L2 patterns (composites of primitives).
|
||||
Each module has an `interface.json` (inputs/outputs, no engine terms) and a
|
||||
`terraform/` directory. `modules/registry.json` indexes every module +
|
||||
version.
|
||||
|
||||
**L1 (primitives — 13):** `s3`, `vpc`, `ecs-cluster`, `ecs-service`,
|
||||
`iam-role`, `alb`, `ecr`, `cloudfront`, `waf`, `rds`, `kms-key`,
|
||||
`dynamodb`, `uptime`.
|
||||
|
||||
**L2 (patterns — 2):**
|
||||
|
||||
| Module | Composes (D-038) | Description |
|
||||
|--------|------------------|-------------|
|
||||
| `microservice` | vpc + ecs-cluster + ecs-service + iam-role + ecr + alb (6 L1s) | Container microservice with a public ALB |
|
||||
| `static-assets` | s3 + cloudfront + kms-key (3 L1s) | Static site fronted by CloudFront |
|
||||
|
||||
L2 modules are opaque stack entries (D-012): the resolver does not expand
|
||||
their children. The L2's `terraform/main.tf` composes L1 modules internally
|
||||
via `module` blocks. See [`docs/modules/index.md`](modules/index.md) for the
|
||||
full catalog and each module's README.
|
||||
@@ -0,0 +1,208 @@
|
||||
# Consumer Guide — Declare intent, deploy to AWS
|
||||
|
||||
This guide walks a consumer through creating a repo, writing a contract,
|
||||
validating it offline, and running it against AWS. It is **generic** across
|
||||
all modules in the registry; `static-assets` is the worked example, but
|
||||
every step applies to `microservice` and any future module.
|
||||
|
||||
## The model
|
||||
|
||||
You write a contract YAML file and the platform does the rest. Your
|
||||
repository contains only your application code and your contracts. You do
|
||||
not write infrastructure modules or adapter code.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["your repo<br/>(app code + contract)"] -->|run_platform.sh| B
|
||||
B["platform<br/>(resolver + adapter + modules)"] -->|contract -> stack -> terraform -> apply| C
|
||||
C["your resources in AWS"]
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **A consumer repository** for your application code + contract.
|
||||
- **A platform-managed environment** bound to your repo. The platform team
|
||||
provisions the AWS account, network, and state backend. See
|
||||
[Environments](environments/index.md).
|
||||
- **AWS credentials** for the target environment, in `.ciagent/.env.secrets`
|
||||
(gitignored) using the `NOVA_AWS_*` prefix. See the
|
||||
[platform README](../README.md#credentials) and
|
||||
[`terraform/bootstrap/README.md`](../terraform/bootstrap/README.md).
|
||||
|
||||
## Step 1 — Create a consumer repo
|
||||
|
||||
Create a repository for your application. The top level holds your app
|
||||
code; your contract lives at the repo root (or wherever you point
|
||||
`run_platform.sh`). Example for a static site:
|
||||
|
||||
```
|
||||
my-static-site/
|
||||
index.html
|
||||
assets/
|
||||
style.css
|
||||
logo.png
|
||||
contract.yml
|
||||
```
|
||||
|
||||
Example for a microservice:
|
||||
|
||||
```
|
||||
my-microservice/
|
||||
app.py
|
||||
Dockerfile
|
||||
contract.yml
|
||||
```
|
||||
|
||||
## Step 2 — Define the contract
|
||||
|
||||
Write `contract.yml`. The `static-assets` example:
|
||||
|
||||
```yaml
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
```
|
||||
|
||||
A `microservice` example:
|
||||
|
||||
```yaml
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
```
|
||||
|
||||
### Contract fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | string | yes | Short operational acronym (`^[a-z][a-z0-9-]{2,5}$`, 3-6 chars). Becomes the stack name used for the Terraform state key and resource naming prefix. Stable across deploys and environment promotions. |
|
||||
| `name` | string | yes | Full human-readable stack name (min 3 chars). |
|
||||
| `environment` | string | yes | The platform-managed environment to deploy to (`dev`/`qa`/`prod`/`dr`). See [Environments](environments/index.md). |
|
||||
| `infrastructure` | array | yes | **Array** of modules to deploy (D-015). Each entry carries a `module` name (matching a registry key), an optional `version` (defaults to latest non-deprecated), and required `inputs`. One entry = single-module deploy; N entries = multi-module manifest. |
|
||||
|
||||
### Infrastructure item fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `module` | string | yes | Module name from `modules/registry.json` (`^[a-z][a-z0-9-]*$`). |
|
||||
| `version` | string | no | Semver pin `X.Y.Z`. Omitted = latest non-deprecated version. |
|
||||
| `inputs` | object | yes | Module-specific inputs (see the module's README). No `aws_*` keys — the contract is engine-agnostic. |
|
||||
|
||||
Each module declares its inputs in its `interface.json`. Consult the
|
||||
[module catalog](modules/index.md) for the full list, or read the module's
|
||||
own README under `modules/l1/<name>/` or `modules/l2/<name>/`.
|
||||
|
||||
The contract is validated against `schemas/contract.schema.json`. An invalid
|
||||
contract (missing field, unknown module, wrong type) fails at the
|
||||
validate-contract stage with a clear error.
|
||||
|
||||
### Interpolation reference (D-016)
|
||||
|
||||
The resolver expands `${env.*}` and `${contract.*}` tokens after the
|
||||
environment is loaded. Unknown tokens raise `ValueError` (fail loud).
|
||||
Expansion is recursive (nested map/list values expand too).
|
||||
|
||||
| Token | Resolves to | Example |
|
||||
|-------|-------------|---------|
|
||||
| `${env.name}` | the environment name | `dev` |
|
||||
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
||||
| `${env.account_id}` | the environment's AWS account id | `000000000000` |
|
||||
| `${env.state_backend.bucket}` | the environment's state bucket | `nova-tfstate-dev-us-east-1` |
|
||||
| `${env.state_backend.lock_table}` | the environment's lock table | `nova-tfstate-locks` |
|
||||
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.0.0.0/16` |
|
||||
| `${contract.id}` | the contract's operational acronym | `stsi` |
|
||||
| `${contract.name}` | the contract's name field | `Static Assets Site` |
|
||||
| `${contract.environment}` | the contract's environment field | `dev` |
|
||||
|
||||
## Step 3 — Validate offline (no AWS required)
|
||||
|
||||
Before touching AWS, validate the contract end-to-end offline. Clone the
|
||||
Nova Platform repo and run `--check-only` against your contract:
|
||||
|
||||
```bash
|
||||
bash scripts/run_platform.sh --check-only path/to/your/contract.yml
|
||||
# Expected: "=== PLATFORM CHECK OK ==="
|
||||
```
|
||||
|
||||
This runs: environment check → contract schema validation → resolve to
|
||||
stack → adapter compiles to HCL → output structure validation. No AWS
|
||||
credentials are needed.
|
||||
|
||||
## Step 4 — Run against live AWS
|
||||
|
||||
Once the contract validates offline, run the full pipeline against AWS.
|
||||
Ensure your credentials are in `.ciagent/.env.secrets` (see
|
||||
[Credentials](../README.md#credentials)):
|
||||
|
||||
```bash
|
||||
bash scripts/run_platform.sh contracts/static-assets.yml
|
||||
# Expected: "=== PLATFORM APPLY OK ==="
|
||||
```
|
||||
|
||||
The full path: environment check → validate contract → resolve to stack →
|
||||
adapter compiles to HCL → load AWS credentials → `terraform init` →
|
||||
`terraform validate` → `terraform plan` → `terraform apply -auto-approve`.
|
||||
|
||||
To stop before apply (review the plan only):
|
||||
|
||||
```bash
|
||||
bash scripts/run_platform.sh --plan-only contracts/static-assets.yml
|
||||
# Expected: "=== PLATFORM PLAN OK ==="
|
||||
```
|
||||
|
||||
## Step 5 — What gets created
|
||||
|
||||
After a successful `dev` run, the resources declared by your module's
|
||||
pattern exist in your AWS account.
|
||||
|
||||
For the `static-assets` example (s3 + cloudfront + kms-key, D-038):
|
||||
|
||||
- An **S3 bucket** (named via your `bucket_name` input, interpolation
|
||||
expanded) with versioning enabled.
|
||||
- A **CloudFront distribution** with the S3 bucket as the origin.
|
||||
- A **KMS key** for SSE.
|
||||
|
||||
For other modules, consult the module's README
|
||||
(`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md`) for the
|
||||
exact resources created.
|
||||
|
||||
## Step 6 — Upload your content (static-assets example)
|
||||
|
||||
The platform provisions the infrastructure; you upload your content. For the
|
||||
`static-assets` module:
|
||||
|
||||
```bash
|
||||
aws s3 sync ./assets s3://<your-bucket-name>/
|
||||
```
|
||||
|
||||
For a `microservice`, the platform provisions the ECS service and ALB; you
|
||||
push your container image to the ECR repo the platform created.
|
||||
|
||||
## Reference
|
||||
|
||||
| Resource | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| Contract schema | `schemas/contract.schema.json` | JSON Schema for consumer contracts. |
|
||||
| Stack schema | `schemas/stack.schema.json` | JSON Schema for the resolved stack instance. |
|
||||
| Module catalog | [modules/](modules/index.md) | All primitives and modules. |
|
||||
| Sample contract | `contracts/static-assets.yml` | The reference example contract. |
|
||||
| Sample contract | `contracts/microservice.yml` | The microservice example contract. |
|
||||
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts. |
|
||||
| Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. |
|
||||
| Terraform adapter | `adapters/terraform/adapter.py` | Compiles stack instances to Terraform. |
|
||||
| Pipeline runner | `scripts/run_platform.sh` | The pipeline runner. |
|
||||
| Environments | [environments/](environments/index.md) | Platform-managed environments. |
|
||||
| Platform README | `README.md` | How the platform works + how to run it. |
|
||||
| Credentials | `README.md#credentials` | The static-key model + bootstrap runbook. |
|
||||
@@ -0,0 +1,139 @@
|
||||
# Contracts
|
||||
|
||||
A consumer declares intent in a **contract** — a small YAML file that names
|
||||
infrastructure (one or more modules), selects an environment, and supplies
|
||||
module-specific inputs. The platform validates, resolves, and deploys it.
|
||||
|
||||
## The contract file
|
||||
|
||||
A minimal example (the `static-assets` module):
|
||||
|
||||
```yaml
|
||||
id: stsi
|
||||
name: Static Assets Site
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
bucket_name: "${env.environment}-${contract.id}-assets"
|
||||
index_document: index.html
|
||||
```
|
||||
|
||||
A `microservice` example:
|
||||
|
||||
```yaml
|
||||
id: msvc
|
||||
name: Microservice
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
service_name: "${env.environment}-${contract.id}-svc"
|
||||
desired_count: 2
|
||||
```
|
||||
|
||||
## Array-based infrastructure (D-015)
|
||||
|
||||
The `infrastructure` field is an **array** of module entries, not a map
|
||||
keyed by module name. This is a locked deviation from the Nova reference
|
||||
(D-015). Each entry is an object with `module`, `version` (optional), and
|
||||
`inputs`.
|
||||
|
||||
One entry = single-module deploy. N entries = multi-module manifest deployed
|
||||
in one pipeline run:
|
||||
|
||||
```yaml
|
||||
id: app
|
||||
name: Pricing Service API
|
||||
environment: dev
|
||||
infrastructure:
|
||||
- module: microservice
|
||||
version: "1.0.0"
|
||||
inputs: { ... }
|
||||
- module: static-assets
|
||||
version: "1.0.0"
|
||||
inputs: { ... }
|
||||
```
|
||||
|
||||
All modules deploy to the same `environment` in one pipeline run.
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | string | yes | Short operational acronym (`^[a-z][a-z0-9-]{2,5}$`, 3-6 chars). Becomes the stack name used for the Terraform state key and resource naming prefix. Stable across deploys and environment promotions. |
|
||||
| `name` | string | yes | Full human-readable stack name (min 3 chars). |
|
||||
| `environment` | string | yes | The platform-managed environment to deploy to (`dev`/`qa`/`prod`/`dr`). See [Environments](../environments/index.md). |
|
||||
| `infrastructure` | array | yes | Array of modules to deploy (D-015). `minItems: 1`. |
|
||||
|
||||
### Infrastructure item fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `module` | string | yes | Module name from `modules/registry.json` (`^[a-z][a-z0-9-]*$`). |
|
||||
| `version` | string | no | Semver pin (`^\d+\.\d+\.\d+$`). Omitted = latest non-deprecated version from the registry. |
|
||||
| `inputs` | object | yes | Module-specific inputs (see the module's README / `interface.json`). No `aws_*` keys — the contract is engine-agnostic. |
|
||||
|
||||
## Validation
|
||||
|
||||
The contract is validated against
|
||||
[`schemas/contract.schema.json`](../../schemas/contract.schema.json). An
|
||||
invalid contract (missing field, unknown module, wrong type) fails at the
|
||||
validate-contract stage with a clear error.
|
||||
|
||||
## Interpolation tokens (D-016)
|
||||
|
||||
The resolver expands `${env.*}` and `${contract.*}` tokens after the
|
||||
environment is loaded. Unknown tokens raise `ValueError` (fail loud).
|
||||
Expansion is recursive (nested map/list values expand too).
|
||||
|
||||
| Token | Resolves to | Example |
|
||||
|-------|-------------|---------|
|
||||
| `${env.name}` | the environment name | `dev` |
|
||||
| `${env.region}` | the environment's AWS region | `us-east-1` |
|
||||
| `${env.account_id}` | the environment's AWS account id | `000000000000` |
|
||||
| `${env.state_backend.bucket}` | the environment's state bucket | `nova-tfstate-dev-us-east-1` |
|
||||
| `${env.state_backend.lock_table}` | the environment's lock table | `nova-tfstate-locks` |
|
||||
| `${env.network.vpc_cidr}` | the environment's VPC CIDR | `10.0.0.0/16` |
|
||||
| `${env.network.azs}` | the environment's availability zones | `["us-east-1a","us-east-1b"]` |
|
||||
| `${contract.id}` | the contract's operational acronym | `stsi` |
|
||||
| `${contract.name}` | the contract's name field | `Static Assets Site` |
|
||||
| `${contract.environment}` | the contract's environment field | `dev` |
|
||||
|
||||
## Sample contracts
|
||||
|
||||
Two reference examples exist in `contracts/`:
|
||||
|
||||
- [`contracts/static-assets.yml`](../../contracts/static-assets.yml) — the
|
||||
`static-assets` module (s3 + cloudfront + kms-key, D-038).
|
||||
- [`contracts/microservice.yml`](../../contracts/microservice.yml) — the
|
||||
`microservice` module (vpc + ecs-cluster + ecs-service + iam-role + ecr +
|
||||
alb, D-038).
|
||||
|
||||
Additionally, every module has a `modules/<name>/examples/` directory with
|
||||
validated example contracts. See the [module catalog](../modules/index.md)
|
||||
for the full list.
|
||||
|
||||
## Per-environment variants (D-033)
|
||||
|
||||
Each sample contract has per-environment variants that differ **only** in
|
||||
the `environment` field — all other fields are identical. Interpolation
|
||||
resolves environment-specific values at resolver time.
|
||||
|
||||
| File | Environment |
|
||||
|------|-------------|
|
||||
| `contracts/static-assets.yml` | dev (default) |
|
||||
| `contracts/static-assets.dev.yml` | dev |
|
||||
| `contracts/static-assets.qa.yml` | qa |
|
||||
| `contracts/static-assets.prod.yml` | prod |
|
||||
| `contracts/static-assets.dr.yml` | dr |
|
||||
| `contracts/microservice.yml` | dev (default) |
|
||||
| `contracts/microservice.dev.yml` | dev |
|
||||
| `contracts/microservice.qa.yml` | qa |
|
||||
| `contracts/microservice.prod.yml` | prod |
|
||||
| `contracts/microservice.dr.yml` | dr |
|
||||
|
||||
Promotion = running the pipeline against the matching variant. See the
|
||||
[Consumer Guide](../consumer-guide.md) for the end-to-end flow.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Environments
|
||||
|
||||
A consumer does **not** provide an AWS account, a VPC, a subnet, or an S3
|
||||
state bucket. The platform manages environments.
|
||||
|
||||
## What an environment is
|
||||
|
||||
A named environment is a **platform-owned** bundle of:
|
||||
|
||||
- An **AWS account** (or a scoped partition of one).
|
||||
- A **network** (VPC + subnets / AZs).
|
||||
- A **state backend** (an S3 bucket + DynamoDB lock table for Terraform
|
||||
state).
|
||||
|
||||
A consumer selects an environment **by name** in their contract:
|
||||
|
||||
```yaml
|
||||
environment: dev
|
||||
```
|
||||
|
||||
The platform resolves the name to the underlying account/network/state
|
||||
backend at run time. The consumer never sees the raw credentials.
|
||||
|
||||
## Environment definition shape (D-018)
|
||||
|
||||
Each environment is a JSON file in `core/environments/`. The field set is
|
||||
reduced from the reference — the IAM role, autonomy, and confidence
|
||||
threshold fields are out of scope for v1.0.
|
||||
|
||||
`core/environments/dev.json` (the sample):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "dev",
|
||||
"description": "Sample dev environment for offline/local testing. account_id placeholder (000000000000) for offline mode.",
|
||||
"account_id": "000000000000",
|
||||
"region": "us-east-1",
|
||||
"state_backend": {
|
||||
"bucket": "nova-tfstate-dev-us-east-1",
|
||||
"lock_table": "nova-tfstate-locks"
|
||||
},
|
||||
"network": {
|
||||
"vpc_cidr": "10.0.0.0/16",
|
||||
"azs": ["us-east-1a", "us-east-1b"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | yes | The environment name (`dev`/`qa`/`prod`/`dr`). |
|
||||
| `description` | string | no | Human-readable description. |
|
||||
| `account_id` | string | yes | The AWS account id (placeholder `000000000000` for offline dev). |
|
||||
| `region` | string | yes | The AWS region. |
|
||||
| `state_backend.bucket` | string | yes | The S3 state bucket name. |
|
||||
| `state_backend.lock_table` | string | yes | The DynamoDB lock table name (`nova-tfstate-locks` per D-022). |
|
||||
| `network.vpc_cidr` | string | yes | The VPC CIDR block. |
|
||||
| `network.azs` | array | yes | The availability zones. |
|
||||
|
||||
### Dropped from the reference (D-018)
|
||||
|
||||
- `runner_role_arn` — identity/authorization is out of scope.
|
||||
- `autonomy` — human-in-the-loop gates are out of scope.
|
||||
- `confidence_threshold` — the confidence signal is out of scope.
|
||||
|
||||
## State backend (D-022)
|
||||
|
||||
The Terraform state backend uses a dedicated DynamoDB lock table named
|
||||
`nova-tfstate-locks` (NOT `nova-outbox` — the audit outbox is out of scope
|
||||
for v1.0). The S3 state bucket is named `nova-tfstate-<account>-<region>`
|
||||
with versioning enabled. See
|
||||
[`terraform/bootstrap/README.md`](../../terraform/bootstrap/README.md) for
|
||||
the bootstrap runbook that creates both.
|
||||
|
||||
## Autonomy by environment
|
||||
|
||||
| Environment | Autonomy | Operator action |
|
||||
|-------------|----------|-----------------|
|
||||
| dev | Fully autonomous | None — `terraform apply -auto-approve` runs automatically. |
|
||||
| qa | Manual | An operator runs `run_platform.sh` against the `qa` contract. |
|
||||
| prod | Manual | An operator runs `run_platform.sh` against the `prod` contract. |
|
||||
| dr | Manual | An operator runs `run_platform.sh` against the `dr` contract. |
|
||||
|
||||
`dev` is the only autonomous environment. Higher environments require a
|
||||
human operator to invoke the pipeline against the environment's contract
|
||||
variant. There are no automated gates or attestation steps — those are out
|
||||
of scope for v1.0. Staging does not exist.
|
||||
|
||||
## Onboarding scaffold (current state)
|
||||
|
||||
The platform repo ships a minimal onboarding scaffold:
|
||||
|
||||
- [`core/environments/`](../../core/environments/) — environment definitions
|
||||
(a sample `dev.json`).
|
||||
- `core/environment_check.py` — checks whether an environment is defined
|
||||
for a given contract's environment name; raises `EnvironmentNotFoundError`
|
||||
when none is defined (D-019).
|
||||
- `scripts/run_platform.sh` calls the check before contract validation.
|
||||
|
||||
The scaffold is minimal: provisioning a new environment is a platform-team
|
||||
action today (bootstrap the state backend + network). Self-service
|
||||
environment provisioning is a future milestone.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Module Catalog
|
||||
|
||||
Every module's full documentation lives next to its code under
|
||||
`modules/l1/<name>/README.md` or `modules/l2/<name>/README.md` (per
|
||||
D-028). This page is the index: it lists the available modules and
|
||||
links to their per-module docs.
|
||||
|
||||
## L1 primitives (13)
|
||||
|
||||
| Module | Stack type | Multi-resource? | Docs |
|
||||
|-----------------|-------------------------------|-----------------|-----------------------------------------------|
|
||||
| `s3` | `aws:s3:bucket` | no | [modules/l1/s3/README.md](../../modules/l1/s3/README.md) |
|
||||
| `vpc` | `aws:ec2:vpc` | yes | [modules/l1/vpc/README.md](../../modules/l1/vpc/README.md) |
|
||||
| `ecs-cluster` | `aws:ecs:cluster` | no | [modules/l1/ecs-cluster/README.md](../../modules/l1/ecs-cluster/README.md) |
|
||||
| `ecs-service` | `aws:ecs:service` | yes | [modules/l1/ecs-service/README.md](../../modules/l1/ecs-service/README.md) |
|
||||
| `iam-role` | `aws:iam:role` | no | [modules/l1/iam-role/README.md](../../modules/l1/iam-role/README.md) |
|
||||
| `alb` | `aws:alb` | yes | [modules/l1/alb/README.md](../../modules/l1/alb/README.md) |
|
||||
| `ecr` | `aws:ecr:repository` | no | [modules/l1/ecr/README.md](../../modules/l1/ecr/README.md) |
|
||||
| `cloudfront` | `aws:cloudfront:distribution` | no | [modules/l1/cloudfront/README.md](../../modules/l1/cloudfront/README.md) |
|
||||
| `waf` | `aws:waf:web_acl` | no | [modules/l1/waf/README.md](../../modules/l1/waf/README.md) |
|
||||
| `rds` | `aws:rds:instance` | no | [modules/l1/rds/README.md](../../modules/l1/rds/README.md) |
|
||||
| `kms-key` | `aws:kms:key` | no | [modules/l1/kms-key/README.md](../../modules/l1/kms-key/README.md) |
|
||||
| `dynamodb` | `aws:dynamodb:table` | no | [modules/l1/dynamodb/README.md](../../modules/l1/dynamodb/README.md) |
|
||||
| `uptime` | `aws:uptime:monitor` | no | [modules/l1/uptime/README.md](../../modules/l1/uptime/README.md) |
|
||||
|
||||
## L2 compositions (2)
|
||||
|
||||
| Module | Composes | Docs |
|
||||
|------------------|---------------------------------------------|---------------------------------------------------|
|
||||
| `microservice` | vpc + ecs-cluster + ecs-service + alb + ecr | [modules/l2/microservice/README.md](../../modules/l2/microservice/README.md) |
|
||||
| `static-assets` | s3 + cloudfront | [modules/l2/static-assets/README.md](../../modules/l2/static-assets/README.md) |
|
||||
|
||||
## See also
|
||||
|
||||
- [modules/README.md](../../modules/README.md) — L1/L2 distinction, registry format, how to add a module.
|
||||
- [modules/README-TEMPLATE.md](../../modules/README-TEMPLATE.md) — per-module doc template.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Module: `<name>`
|
||||
|
||||
> Copy this template into `modules/l1/<name>/README.md` or
|
||||
> `modules/l2/<name>/README.md` and fill in the placeholders. Sections
|
||||
> marked **DROP** are intentionally omitted from nova modules
|
||||
> (D-029): do **not** add `NFRs` or `Compliance` sections.
|
||||
|
||||
## Overview
|
||||
|
||||
One-paragraph description of what this module provisions, the stack
|
||||
type(s) it exposes, and when to reach for it. Mention whether it is L1
|
||||
(single primitive) or L2 (composition of L1s), and whether it is
|
||||
multi-resource.
|
||||
|
||||
- **Stack type:** `aws:<service>:<resource>`
|
||||
- **Kind:** `l1` (or `l2`)
|
||||
- **Version:** `1.0.0`
|
||||
|
||||
## Resources
|
||||
|
||||
List the concrete cloud resources the Terraform adapter creates. For L1
|
||||
single-resource modules this is one row; for multi-resource L1s mirror
|
||||
the `resources[]` array in `interface.json`.
|
||||
|
||||
| Stack type | Terraform resource | Notes |
|
||||
|-------------------------|------------------------------------|----------------------------------|
|
||||
| `aws:s3:bucket` | `aws_s3_bucket` | The bucket itself |
|
||||
| `aws:s3:bucket` | `aws_s3_bucket_versioning` | Versioning sibling |
|
||||
| `aws:s3:bucket` | `aws_s3_bucket_server_side_encryption_configuration` | SSE config sibling |
|
||||
|
||||
For L2 modules, list the L1 modules composed via `module` blocks in
|
||||
`terraform/main.tf` instead.
|
||||
|
||||
## Inputs
|
||||
|
||||
Mirror `interface.json` → `inputs`. Mark required inputs with **yes**.
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|----------------|---------|----------|---------------|-----------------------------------|
|
||||
| `bucket_name` | string | yes | — | Globally-unique S3 bucket name |
|
||||
| `region` | string | yes | — | AWS region |
|
||||
| `kms_key_arn` | string | no | `null` | CMK ARN for SSE-KMS |
|
||||
| `enabled` | boolean | no | `true` | Feature flag |
|
||||
| `tags` | map | no | `{}` | Tags merged with module defaults |
|
||||
|
||||
## Outputs
|
||||
|
||||
Mirror `interface.json` → `outputs`.
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------------------------------|--------|----------------------------------------------|
|
||||
| `bucket_arn` | arn | The S3 bucket ARN |
|
||||
| `bucket_name` | string | The bucket name |
|
||||
| `bucket_regional_domain_name` | string | The bucket regional domain name |
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "bucket" {
|
||||
source = "modules/l1/s3/terraform"
|
||||
|
||||
bucket_name = "nova-prod-assets"
|
||||
region = "us-east-1"
|
||||
|
||||
tags = {
|
||||
"nova:owner" = "team-platform"
|
||||
"nova:environment" = "prod"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or as a flat-stack contract entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "s3",
|
||||
"version": "1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "nova-prod-assets",
|
||||
"region": "us-east-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
This module follows the registry semver contract: bump the patch/minor
|
||||
version in `interface.json` and `modules/registry.json` for any
|
||||
input/output/behavior change. Breaking changes (renamed inputs,
|
||||
removed outputs, changed defaults) require a major bump and a new
|
||||
registry entry; the previous version is marked `deprecated: true` and
|
||||
remains selectable by pinned contracts. See `modules/README.md` for
|
||||
the registry format and the resolver's version-selection rules.
|
||||
|
||||
<!-- DROP: NFRs — out of scope for nova v1 (D-029) -->
|
||||
<!-- DROP: Compliance / attestation — out of scope for nova v1 (D-029) -->
|
||||
@@ -0,0 +1,127 @@
|
||||
# Nova Modules
|
||||
|
||||
Nova ships a two-tier module library. Modules are **engine-agnostic**:
|
||||
their contract is declared in `interface.json` (stack types like
|
||||
`aws:s3:bucket`), and an adapter translates the contract to a concrete
|
||||
IaC engine (Terraform today; Pulumi/CDK possible later). All L1 modules
|
||||
in this repo ship a Terraform adapter under `terraform/`.
|
||||
|
||||
## L1 vs L2
|
||||
|
||||
| Tier | What it is | Composes | Examples |
|
||||
|------|----------------------------------------------------------------------------|---------------------|-----------------------------------|
|
||||
| L1 | A single primitive resource (or tightly-coupled resource group) on a cloud | One stack resource | `s3`, `vpc`, `ecs-cluster`, `alb` |
|
||||
| L2 | A composition of L1s expressing an architectural pattern | Multiple L1 modules | `microservice`, `static-assets` |
|
||||
|
||||
- **L1** = one entry in the flat stack. Even multi-resource L1s (e.g.
|
||||
`vpc`, `ecs-service`, `alb`) emit a single stack entry; their
|
||||
`interface.json` lists the child resources in a `resources[]` array
|
||||
for documentation, but the resolver does **not** expand them
|
||||
(D-012).
|
||||
- **L2** = also one opaque entry in the flat stack. The L2's
|
||||
`terraform/main.tf` composes L1 modules internally via `module` blocks
|
||||
(D-012). The L2 exposes its own L2-level `inputs`/`outputs`; children
|
||||
and wiring live in terraform, not in the interface.
|
||||
|
||||
## Registry format
|
||||
|
||||
`modules/registry.json` maps `module_name -> version -> entry`:
|
||||
|
||||
```json
|
||||
{
|
||||
"s3": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l1/s3/interface.json",
|
||||
"terraform_dir": "modules/l1/s3/terraform",
|
||||
"published_at": "2026-08-20T00:00:00Z",
|
||||
"deprecated": false,
|
||||
"kind": "l1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `interface` — path to the `interface.json` declaring the contract.
|
||||
- `terraform_dir` — path to the adapter's Terraform module directory
|
||||
(the flat stack's `source` field).
|
||||
- `kind` — `"l1"` or `"l2"`.
|
||||
- `deprecated` — when `true`, the resolver warns and selects the latest
|
||||
non-deprecated version unless the caller pins a version.
|
||||
|
||||
## interface.json shape (D-014)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "s3",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:s3:bucket",
|
||||
"description": "...",
|
||||
"inputs": { "<name>": { "type": "...", "required": true, "description": "..." } },
|
||||
"outputs": { "<name>": { "type": "...", "description": "..." } },
|
||||
"resources": [ { "type": "aws:ec2:vpc", "inputs": [...], "outputs": [...] } ]
|
||||
}
|
||||
```
|
||||
|
||||
- `type` is **stack-typed** — `aws:<service>:<resource>` (e.g.
|
||||
`aws:s3:bucket`), **not** the Terraform resource name
|
||||
(`aws_s3_bucket`). The adapter performs the translation.
|
||||
- `resources[]` is present only on multi-resource L1s (`vpc`,
|
||||
`ecs-service`, `alb`); it documents the child stack types but does not
|
||||
drive resolution.
|
||||
- **Dropped** per D-014: `nfrs` (confidence signal, out of scope) and
|
||||
`intra_refs` (wire engine, eliminated by D-012). Do not re-add them.
|
||||
|
||||
## Conventions shared by all L1 Terraform adapters
|
||||
|
||||
- `terraform/versions.tf` pins `required_version = ">= 1.9, < 1.10"` and
|
||||
`aws ~> 5.0`.
|
||||
- Every resource is guarded by `count = var.enabled ? 1 : 0`; the
|
||||
`enabled` input defaults to `true`.
|
||||
- `locals.tf` merges module-default tags with caller-supplied `var.tags`:
|
||||
```hcl
|
||||
tags = merge({ "nova:owner" = "nova", "nova:environment" = "dev" }, var.tags)
|
||||
```
|
||||
- Every `interface.json` input has a matching `variable` block; every
|
||||
output has a matching `output` block. Outputs return `null` (or `[]`)
|
||||
when `enabled = false`.
|
||||
|
||||
## How to add a module
|
||||
|
||||
1. Pick the tier. New primitive → L1. New pattern composing existing
|
||||
L1s → L2.
|
||||
2. Create `modules/l1/<name>/` (or `modules/l2/<name>/`).
|
||||
3. Author `interface.json` (L1) or `interface.json` + L2 terraform that
|
||||
composes L1s via `module` blocks. Use `modules/README-TEMPLATE.md`
|
||||
as the per-module doc template.
|
||||
4. Author `terraform/{main,variables,outputs,versions,locals}.tf`
|
||||
following the conventions above.
|
||||
5. Add an entry to `modules/registry.json` and a row to the catalog at
|
||||
`docs/modules/index.md`.
|
||||
6. Verify: `python3 -c "import json; json.load(open('modules/l1/<name>/interface.json'))"`
|
||||
and `terraform validate` inside `terraform/`.
|
||||
|
||||
## L1 primitives (13)
|
||||
|
||||
| Module | Stack type | Multi-resource? | Description |
|
||||
|-----------------|-------------------------------|-----------------|----------------------------------------------------------|
|
||||
| `s3` | `aws:s3:bucket` | no | S3 bucket with versioning + SSE-KMS |
|
||||
| `vpc` | `aws:ec2:vpc` | yes | VPC + subnets + route table + IGW |
|
||||
| `ecs-cluster` | `aws:ecs:cluster` | no | ECS cluster |
|
||||
| `ecs-service` | `aws:ecs:service` | yes | ECS task definition + service |
|
||||
| `iam-role` | `aws:iam:role` | no | IAM role with assume-role policy |
|
||||
| `alb` | `aws:alb` | yes | ALB + target group + listener |
|
||||
| `ecr` | `aws:ecr:repository` | no | ECR repository with scan-on-push |
|
||||
| `cloudfront` | `aws:cloudfront:distribution` | no | CloudFront distribution with a single origin |
|
||||
| `waf` | `aws:waf:web_acl` | no | WAFv2 web ACL (regional, default allow) |
|
||||
| `rds` | `aws:rds:instance` | no | RDS Postgres DB instance |
|
||||
| `kms-key` | `aws:kms:key` | no | KMS CMK with alias |
|
||||
| `dynamodb` | `aws:dynamodb:table` | no | DynamoDB table (PAY_PER_REQUEST default) |
|
||||
| `uptime` | `aws:uptime:monitor` | no | Uptime monitor (CloudWatch alarm stand-in) |
|
||||
|
||||
## L2 compositions (2)
|
||||
|
||||
| Module | Composes | Description |
|
||||
|------------------|-------------------------------------------|----------------------------------------------|
|
||||
| `microservice` | vpc + ecs-cluster + ecs-service + iam-role + ecr + alb | Container microservice with public ALB (D-038) |
|
||||
| `static-assets` | s3 + cloudfront + kms-key | Static site fronted by CloudFront (D-038) |
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: alb
|
||||
|
||||
Application Load Balancer primitive (multi-resource: LB + target group + listener; stack type `aws:alb`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "alb",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:alb",
|
||||
"description": "Application Load Balancer primitive (multi-resource: LB + target group + listener). Engine-agnostic stack types aws:alb + aws:alb:targetgroup + aws:alb:listener; the Terraform adapter translates to aws_lb/aws_lb_target_group/aws_lb_listener.",
|
||||
"inputs": {
|
||||
"lb_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the load balancer.",
|
||||
"required": true
|
||||
},
|
||||
"subnet_ids": {
|
||||
"type": "list",
|
||||
"description": "List of subnet ids the LB is deployed into.",
|
||||
"required": true
|
||||
},
|
||||
"target_group_port": {
|
||||
"type": "integer",
|
||||
"default": 80,
|
||||
"description": "Port the target group forwards to."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"lb_arn": {
|
||||
"type": "arn",
|
||||
"description": "The load balancer ARN."
|
||||
},
|
||||
"dns_name": {
|
||||
"type": "string",
|
||||
"description": "The load balancer DNS name."
|
||||
},
|
||||
"target_group_arn": {
|
||||
"type": "arn",
|
||||
"description": "The target group ARN."
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:alb",
|
||||
"description": "The Application Load Balancer.",
|
||||
"inputs": ["lb_name", "subnet_ids"],
|
||||
"outputs": ["lb_arn", "dns_name"]
|
||||
},
|
||||
{
|
||||
"type": "aws:alb:targetgroup",
|
||||
"description": "Target group on the LB port.",
|
||||
"inputs": ["lb_name", "target_group_port"],
|
||||
"outputs": ["target_group_arn"]
|
||||
},
|
||||
{
|
||||
"type": "aws:alb:listener",
|
||||
"description": "Listener forwarding to the target group.",
|
||||
"inputs": ["target_group_port", "target_group_arn"],
|
||||
"outputs": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
# Target group requires a vpc_id. The L1 interface does not expose it as
|
||||
# an input by design (kept minimal per D-014); the caller is expected to
|
||||
# supply subnets in a single VPC. When a vpc_id input is added later, this
|
||||
# local can be removed. For now, null forces the caller to set it via a
|
||||
# provider-level default or an extension.
|
||||
vpc_id = null
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
resource "aws_lb" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.lb_name
|
||||
load_balancer_type = "application"
|
||||
subnets = var.subnet_ids
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_lb_target_group" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name_prefix = "${var.lb_name}-"
|
||||
port = var.target_group_port
|
||||
protocol = "HTTP"
|
||||
target_type = "ip"
|
||||
vpc_id = local.vpc_id
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_lb_listener" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
load_balancer_arn = aws_lb.this[0].id
|
||||
port = var.target_group_port
|
||||
protocol = "HTTP"
|
||||
|
||||
default_action {
|
||||
type = "forward"
|
||||
target_group_arn = aws_lb_target_group.this[0].arn
|
||||
}
|
||||
|
||||
depends_on = [aws_lb_target_group.this]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
output "lb_arn" {
|
||||
value = var.enabled ? aws_lb.this[0].arn : null
|
||||
description = "The load balancer ARN."
|
||||
}
|
||||
|
||||
output "dns_name" {
|
||||
value = var.enabled ? aws_lb.this[0].dns_name : null
|
||||
description = "The load balancer DNS name."
|
||||
}
|
||||
|
||||
output "target_group_arn" {
|
||||
value = var.enabled ? aws_lb_target_group.this[0].arn : null
|
||||
description = "The target group ARN."
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
variable "lb_name" {
|
||||
type = string
|
||||
description = "Name of the load balancer."
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
type = list(string)
|
||||
description = "List of subnet ids the LB is deployed into."
|
||||
}
|
||||
|
||||
variable "target_group_port" {
|
||||
type = number
|
||||
description = "Port the target group forwards to."
|
||||
default = 80
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: cloudfront
|
||||
|
||||
CloudFront distribution primitive (stack type `aws:cloudfront:distribution`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "cloudfront",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:cloudfront:distribution",
|
||||
"description": "CloudFront distribution primitive (engine-agnostic stack type aws:cloudfront:distribution; the Terraform adapter translates to aws_cloudfront_distribution).",
|
||||
"inputs": {
|
||||
"distribution_name": {
|
||||
"type": "string",
|
||||
"description": "Name (comment) of the CloudFront distribution.",
|
||||
"required": true
|
||||
},
|
||||
"origin_domain": {
|
||||
"type": "string",
|
||||
"description": "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS).",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"distribution_arn": {
|
||||
"type": "arn",
|
||||
"description": "The CloudFront distribution ARN."
|
||||
},
|
||||
"domain_name": {
|
||||
"type": "string",
|
||||
"description": "The CloudFront distribution domain name."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
|
||||
origin_id = "${var.distribution_name}-origin"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
resource "aws_cloudfront_distribution" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
comment = var.distribution_name
|
||||
enabled = true
|
||||
price_class = "PriceClass_100"
|
||||
tags = local.tags
|
||||
|
||||
origin {
|
||||
domain_name = var.origin_domain
|
||||
origin_id = local.origin_id
|
||||
}
|
||||
|
||||
default_cache_behavior {
|
||||
allowed_methods = ["GET", "HEAD", "OPTIONS"]
|
||||
cached_methods = ["GET", "HEAD"]
|
||||
target_origin_id = local.origin_id
|
||||
|
||||
forwarded_values {
|
||||
query_string = false
|
||||
|
||||
cookies {
|
||||
forward = "none"
|
||||
}
|
||||
}
|
||||
|
||||
viewer_protocol_policy = "redirect-to-https"
|
||||
min_ttl = 0
|
||||
default_ttl = 3600
|
||||
max_ttl = 86400
|
||||
}
|
||||
|
||||
restrictions {
|
||||
geo_restriction {
|
||||
restriction_type = "none"
|
||||
}
|
||||
}
|
||||
|
||||
viewer_certificate {
|
||||
cloudfront_default_certificate = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "distribution_arn" {
|
||||
value = var.enabled ? aws_cloudfront_distribution.this[0].arn : null
|
||||
description = "The CloudFront distribution ARN."
|
||||
}
|
||||
|
||||
output "domain_name" {
|
||||
value = var.enabled ? aws_cloudfront_distribution.this[0].domain_name : null
|
||||
description = "The CloudFront distribution domain name."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "distribution_name" {
|
||||
type = string
|
||||
description = "Name (comment) of the CloudFront distribution."
|
||||
}
|
||||
|
||||
variable "origin_domain" {
|
||||
type = string
|
||||
description = "Domain name of the origin (e.g. an S3 bucket regional domain or ALB DNS)."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: dynamodb
|
||||
|
||||
DynamoDB table primitive (stack type `aws:dynamodb:table`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "dynamodb",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:dynamodb:table",
|
||||
"description": "DynamoDB table primitive (engine-agnostic stack type aws:dynamodb:table; the Terraform adapter translates to aws_dynamodb_table).",
|
||||
"inputs": {
|
||||
"table_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the DynamoDB table.",
|
||||
"required": true
|
||||
},
|
||||
"hash_key": {
|
||||
"type": "string",
|
||||
"description": "Name of the partition (hash) key.",
|
||||
"required": true
|
||||
},
|
||||
"billing_mode": {
|
||||
"type": "string",
|
||||
"default": "PAY_PER_REQUEST",
|
||||
"description": "Billing mode: PAY_PER_REQUEST or PROVISIONED."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"table_arn": {
|
||||
"type": "arn",
|
||||
"description": "The DynamoDB table ARN."
|
||||
},
|
||||
"table_name": {
|
||||
"type": "string",
|
||||
"description": "The DynamoDB table name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_dynamodb_table" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.table_name
|
||||
billing_mode = var.billing_mode
|
||||
hash_key = var.hash_key
|
||||
tags = local.tags
|
||||
|
||||
attribute {
|
||||
name = var.hash_key
|
||||
type = "S"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "table_arn" {
|
||||
value = var.enabled ? aws_dynamodb_table.this[0].arn : null
|
||||
description = "The DynamoDB table ARN."
|
||||
}
|
||||
|
||||
output "table_name" {
|
||||
value = var.enabled ? aws_dynamodb_table.this[0].name : null
|
||||
description = "The DynamoDB table name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
variable "table_name" {
|
||||
type = string
|
||||
description = "Name of the DynamoDB table."
|
||||
}
|
||||
|
||||
variable "hash_key" {
|
||||
type = string
|
||||
description = "Name of the partition (hash) key."
|
||||
}
|
||||
|
||||
variable "billing_mode" {
|
||||
type = string
|
||||
description = "Billing mode: PAY_PER_REQUEST or PROVISIONED."
|
||||
default = "PAY_PER_REQUEST"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecr
|
||||
|
||||
ECR repository primitive (stack type `aws:ecr:repository`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "ecr",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecr:repository",
|
||||
"description": "ECR repository primitive (engine-agnostic stack type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
||||
"inputs": {
|
||||
"repository_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECR repository.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"repository_url": {
|
||||
"type": "string",
|
||||
"description": "The ECR repository URL."
|
||||
},
|
||||
"repository_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECR repository ARN."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
resource "aws_ecr_repository" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.repository_name
|
||||
image_tag_mutability = "MUTABLE"
|
||||
tags = local.tags
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "repository_url" {
|
||||
value = var.enabled ? aws_ecr_repository.this[0].repository_url : null
|
||||
description = "The ECR repository URL."
|
||||
}
|
||||
|
||||
output "repository_arn" {
|
||||
value = var.enabled ? aws_ecr_repository.this[0].arn : null
|
||||
description = "The ECR repository ARN."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "repository_name" {
|
||||
type = string
|
||||
description = "Name of the ECR repository."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecs-cluster
|
||||
|
||||
ECS cluster primitive (stack type `aws:ecs:cluster`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "ecs-cluster",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:cluster",
|
||||
"description": "ECS cluster primitive (engine-agnostic stack type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
||||
"inputs": {
|
||||
"cluster_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECS cluster.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"cluster_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECS cluster ARN."
|
||||
},
|
||||
"cluster_name": {
|
||||
"type": "string",
|
||||
"description": "The ECS cluster name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
resource "aws_ecs_cluster" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.cluster_name
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "cluster_arn" {
|
||||
value = var.enabled ? aws_ecs_cluster.this[0].arn : null
|
||||
description = "The ECS cluster ARN."
|
||||
}
|
||||
|
||||
output "cluster_name" {
|
||||
value = var.enabled ? aws_ecs_cluster.this[0].name : null
|
||||
description = "The ECS cluster name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "cluster_name" {
|
||||
type = string
|
||||
description = "Name of the ECS cluster."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: ecs-service
|
||||
|
||||
ECS service primitive (multi-resource: task definition + service; stack type `aws:ecs:service`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "ecs-service",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:service",
|
||||
"description": "ECS service primitive (multi-resource: task definition + service). Engine-agnostic stack types aws:ecs:taskdef + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service.",
|
||||
"inputs": {
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ECS service (also used as the task definition family).",
|
||||
"required": true
|
||||
},
|
||||
"cluster_arn": {
|
||||
"type": "arn",
|
||||
"description": "ARN of the ECS cluster the service runs in.",
|
||||
"required": true
|
||||
},
|
||||
"task_definition": {
|
||||
"type": "string",
|
||||
"description": "Task definition ARN or family:revision to run. If supplied as a path/string JSON, the module creates an aws_ecs_task_definition.",
|
||||
"required": true
|
||||
},
|
||||
"desired_count": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "Number of tasks to run."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"service_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ECS service ARN."
|
||||
},
|
||||
"service_name": {
|
||||
"type": "string",
|
||||
"description": "The ECS service name (echoes the input)."
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:ecs:taskdef",
|
||||
"description": "The ECS task definition (registered from task_definition input).",
|
||||
"inputs": ["service_name", "task_definition"],
|
||||
"outputs": []
|
||||
},
|
||||
{
|
||||
"type": "aws:ecs:service",
|
||||
"description": "The ECS service running the task definition on the cluster.",
|
||||
"inputs": ["service_name", "cluster_arn", "desired_count"],
|
||||
"outputs": ["service_arn", "service_name"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
resource "aws_ecs_task_definition" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
family = var.service_name
|
||||
container_definitions = var.task_definition
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.service_name
|
||||
cluster = var.cluster_arn
|
||||
task_definition = aws_ecs_task_definition.this[0].arn
|
||||
desired_count = var.desired_count
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "service_arn" {
|
||||
value = var.enabled ? aws_ecs_service.this[0].id : null
|
||||
description = "The ECS service ARN."
|
||||
}
|
||||
|
||||
output "service_name" {
|
||||
value = var.enabled ? aws_ecs_service.this[0].name : null
|
||||
description = "The ECS service name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
variable "service_name" {
|
||||
type = string
|
||||
description = "Name of the ECS service (also used as the task definition family)."
|
||||
}
|
||||
|
||||
variable "cluster_arn" {
|
||||
type = string
|
||||
description = "ARN of the ECS cluster the service runs in."
|
||||
}
|
||||
|
||||
variable "task_definition" {
|
||||
type = string
|
||||
description = "Task definition JSON string (container definitions). The module registers an aws_ecs_task_definition with family = service_name."
|
||||
}
|
||||
|
||||
variable "desired_count" {
|
||||
type = number
|
||||
description = "Number of tasks to run."
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: iam-role
|
||||
|
||||
IAM role primitive (stack type `aws:iam:role`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "iam-role",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:iam:role",
|
||||
"description": "IAM role primitive (engine-agnostic stack type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
||||
"inputs": {
|
||||
"role_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the IAM role.",
|
||||
"required": true
|
||||
},
|
||||
"policy_document": {
|
||||
"type": "string",
|
||||
"description": "Assume-role policy document JSON string.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"role_arn": {
|
||||
"type": "arn",
|
||||
"description": "The IAM role ARN."
|
||||
},
|
||||
"role_name": {
|
||||
"type": "string",
|
||||
"description": "The IAM role name (echoes the input)."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
resource "aws_iam_role" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = var.role_name
|
||||
assume_role_policy = var.policy_document
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "role_arn" {
|
||||
value = var.enabled ? aws_iam_role.this[0].arn : null
|
||||
description = "The IAM role ARN."
|
||||
}
|
||||
|
||||
output "role_name" {
|
||||
value = var.enabled ? aws_iam_role.this[0].name : null
|
||||
description = "The IAM role name (echoes the input)."
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
variable "role_name" {
|
||||
type = string
|
||||
description = "Name of the IAM role."
|
||||
}
|
||||
|
||||
variable "policy_document" {
|
||||
type = string
|
||||
description = "Assume-role policy document JSON string."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: kms-key
|
||||
|
||||
KMS customer master key primitive (stack type `aws:kms:key`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "kms-key",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:kms:key",
|
||||
"description": "KMS customer master key primitive (engine-agnostic stack type aws:kms:key; the Terraform adapter translates to aws_kms_key).",
|
||||
"inputs": {
|
||||
"key_name": {
|
||||
"type": "string",
|
||||
"description": "Name (alias) of the KMS key.",
|
||||
"required": true
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"key_arn": {
|
||||
"type": "arn",
|
||||
"description": "The KMS key ARN."
|
||||
},
|
||||
"key_id": {
|
||||
"type": "string",
|
||||
"description": "The KMS key id."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_kms_key" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
description = "KMS key managed by nova L1 kms-key primitive."
|
||||
deletion_window_in_days = 30
|
||||
tags = local.tags
|
||||
}
|
||||
|
||||
resource "aws_kms_alias" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
name = "alias/${var.key_name}"
|
||||
target_key_id = aws_kms_key.this[0].key_id
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "key_arn" {
|
||||
value = var.enabled ? aws_kms_key.this[0].arn : null
|
||||
description = "The KMS key ARN."
|
||||
}
|
||||
|
||||
output "key_id" {
|
||||
value = var.enabled ? aws_kms_key.this[0].key_id : null
|
||||
description = "The KMS key id."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
variable "key_name" {
|
||||
type = string
|
||||
description = "Name (alias) of the KMS key."
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.9, < 1.10"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# L1: rds
|
||||
|
||||
RDS DB instance primitive (stack type `aws:rds:instance`). See `interface.json` for the full contract and `README-TEMPLATE.md` for the canonical section layout.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "rds",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:rds:instance",
|
||||
"description": "RDS DB instance primitive (engine-agnostic stack type aws:rds:instance; the Terraform adapter translates to aws_db_instance).",
|
||||
"inputs": {
|
||||
"instance_name": {
|
||||
"type": "string",
|
||||
"description": "Name (identifier) of the RDS DB instance.",
|
||||
"required": true
|
||||
},
|
||||
"instance_class": {
|
||||
"type": "string",
|
||||
"default": "db.t3.micro",
|
||||
"description": "DB instance class."
|
||||
},
|
||||
"allocated_storage": {
|
||||
"type": "integer",
|
||||
"default": 20,
|
||||
"description": "Allocated storage in GiB."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
},
|
||||
"tags": {
|
||||
"type": "map",
|
||||
"default": {},
|
||||
"description": "Additional tags to merge with the module defaults."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"instance_endpoint": {
|
||||
"type": "string",
|
||||
"description": "The RDS DB instance endpoint (host:port)."
|
||||
},
|
||||
"instance_arn": {
|
||||
"type": "arn",
|
||||
"description": "The RDS DB instance ARN."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
locals {
|
||||
tags = merge(
|
||||
{
|
||||
"nova:owner" = "nova"
|
||||
"nova:environment" = "dev"
|
||||
},
|
||||
var.tags,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
resource "aws_db_instance" "this" {
|
||||
count = var.enabled ? 1 : 0
|
||||
identifier = var.instance_name
|
||||
instance_class = var.instance_class
|
||||
allocated_storage = var.allocated_storage
|
||||
engine = "postgres"
|
||||
engine_version = "14"
|
||||
username = "nova"
|
||||
password = "changeme-rotate-me"
|
||||
skip_final_snapshot = true
|
||||
tags = local.tags
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "instance_endpoint" {
|
||||
value = var.enabled ? aws_db_instance.this[0].endpoint : null
|
||||
description = "The RDS DB instance endpoint (host:port)."
|
||||
}
|
||||
|
||||
output "instance_arn" {
|
||||
value = var.enabled ? aws_db_instance.this[0].arn : null
|
||||
description = "The RDS DB instance ARN."
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
variable "instance_name" {
|
||||
type = string
|
||||
description = "Name (identifier) of the RDS DB instance."
|
||||
}
|
||||
|
||||
variable "instance_class" {
|
||||
type = string
|
||||
description = "DB instance class."
|
||||
default = "db.t3.micro"
|
||||
}
|
||||
|
||||
variable "allocated_storage" {
|
||||
type = number
|
||||
description = "Allocated storage in GiB."
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
description = "Additional tags to merge with the module defaults."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "enabled" {
|
||||
type = bool
|
||||
description = "Feature flag: enable/disable this module. Set to false to skip resource creation."
|
||||
default = true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user