diff --git a/.ciagent/ARCHITECTURE.md b/.ciagent/ARCHITECTURE.md index c9ca077..d0301b4 100644 --- a/.ciagent/ARCHITECTURE.md +++ b/.ciagent/ARCHITECTURE.md @@ -248,7 +248,7 @@ The spike (Phases 08–10) materializes the **minimum** that proves the IR commitments hold (no polyglot mess): - One L1: `l1-s3` (IR-typed interface; the only AWS resource in the spike). -- One L2 thin-composition: `l2-static-asset` (references `l1-s3` only). +- One L2 thin-composition: `l2-static-assets` (references `l1-s3` only). - Terraform adapter: IR → `terraform plan` against AWS via OIDC. - One contract submission → contract→IR → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB @@ -297,7 +297,7 @@ ACDL has no `package.json`. The verification gate substitutes: 2. Phase 07 — finalize architecture v1.0; author schemas + designs. 3. Phase 08 — AWS OIDC bootstrap (use temp key once, rotate). 4. Phase 09 — IR + `l1-s3` + Terraform adapter → `terraform plan`. -5. Phase 10 — `l2-static-asset` + contract→IR → end-to-end spike. +5. Phase 10 — `l2-static-assets` + contract→IR → end-to-end spike. 6. COMPLETE gate — review → ship `v1.2.0` → audit. **DONE.** ## v1.2 build-out scope diff --git a/.ciagent/PROJECT.md b/.ciagent/PROJECT.md index 2137a71..1a741e4 100644 --- a/.ciagent/PROJECT.md +++ b/.ciagent/PROJECT.md @@ -57,7 +57,7 @@ Finalize the architecture to v1.0 (resolve all 11 open design decisions in end-to-end v1 implementation spike: - **One L1 module** (`l1-s3`) — substrate-agnostic, IR-typed interface. -- **One L2 thin-composition** (`l2-static-asset`) — references the L1. +- **One L2 thin-composition** (`l2-static-assets`) — references the L1. - **Terraform adapter** — compiles the IR to a real `terraform plan` against AWS via OIDC (no long-lived credentials, per §12.5). - **One contract submission** → contract→IR resolution → @@ -79,7 +79,7 @@ id 202 published. D-034 closed (root key deactivated by user).** | 07 | architecture-v1-finalization | Resolve the 11 open decisions → architecture v1.0. Author IR JSON Schema, PolicyCheckResult schema, contract schema, confidence-signal spec, HITL matrix, outbox/ledger design under `schemas/` + `platform/`. | | 08 | aws-oidc-bootstrap | One-shot use of a temporary long-lived key (waiver D-034) to create an IAM role + OIDC trust policy for the act_runner, an S3 state bucket, and a DynamoDB lock table. Rotate the key. Verify the runner assumes the role via OIDC with no long-lived secret. | | 09 | v1-spike-ir-and-l1-and-adapter | Target Stack IR; one real L1 (`l1-s3`) with IR-typed interface; L1 registry; Terraform adapter (IR → Terraform var/output + `terraform plan`) running against AWS via OIDC. | -| 10 | v1-spike-l2-and-contract-e2e | One L2 thin-composition (`l2-static-asset`) referencing `l1-s3`; contract schema + contract→IR resolution; one end-to-end contract submission → `terraform plan` → Checkov → confidence signal → evidence event to outbox. Verify the IR commitments hold. | +| 10 | v1-spike-l2-and-contract-e2e | One L2 thin-composition (`l2-static-assets`) referencing `l1-s3`; contract schema + contract→IR resolution; one end-to-end contract submission → `terraform plan` → Checkov → confidence signal → evidence event to outbox. Verify the IR commitments hold. | Milestone COMPLETE gate: review → ship `v1.2.0` (feature milestone, next minor per ship.md) → audit. **DONE.** @@ -173,7 +173,7 @@ Production platform + contract ingestion + pipeline maturation. The v1.6 milestone left the platform documented and environments-aware; v1.7 takes it to a production-grade platform. 12 user-directed scope axes (2026-07-22): -1. **Rename `static-asset` → `static-assets`** (D-048 — including +1. **Rename `static-assets` → `static-assets`** (D-048 — including `.ciagent/` historical narrative, overriding the v1.6 preservation precedent). The reconstruction test is updated to expect `static-assets`. 2. **Augment `static-assets` to a production-ready stack** by authoring a @@ -226,7 +226,7 @@ to a production-grade platform. 12 user-directed scope axes (2026-07-22): | Phase | Name | Goal | |-------|------|------| -| 22 | rename-and-production-static-assets-stack | Rename `static-asset` → `static-assets` everywhere (D-048). Author `cloudfront` + `waf` primitives. Augment `static-assets` to S3 + CloudFront (OAC) + WAF (D-049). Expand adapter. Bump `uses:` to `@v1.6`; create floating `v1.6` + `v1` tags (D-057). | +| 22 | rename-and-production-static-assets-stack | Rename `static-assets` → `static-assets` everywhere (D-048). Author `cloudfront` + `waf` primitives. Augment `static-assets` to S3 + CloudFront (OAC) + WAF (D-049). Expand adapter. Bump `uses:` to `@v1.6`; create floating `v1.6` + `v1` tags (D-057). | | 23 | tagging-standards-and-security-adapters | Required-tag set + Checkov custom rule (D-054, D-043 closure). Wiz adapter stub (D-052). Kyverno K8s-native adapter (D-053). Schema engine enum updated. | | 24 | platform-lambda-and-contract-ingestion | Platform Lambda + DynamoDB `acdl-contracts` table (D-051) + cross-account IAM + onboarding grant. | | 25 | deploy-pipeline-dx-outputs-and-error-reporting | SSM SecureString + PR comment outputs (D-050). Lambda `report_error` → GitHub issue (D-055). Stage comments after each successful stage. | @@ -262,7 +262,7 @@ New requirements REQ-16..REQ-28 — see `REQUIREMENTS.md` §v1.1. Summary: - **REQ-23:** AWS OIDC bootstrap (IAM role + trust policy for act_runner); the long-lived key is used once then rotated (waiver D-034). - **REQ-24:** One real L1 module (`l1-s3`) with an IR-typed interface. -- **REQ-25:** One real L2 thin-composition (`l2-static-asset`) referencing +- **REQ-25:** One real L2 thin-composition (`l2-static-assets`) referencing `l1-s3`. - **REQ-26:** Terraform adapter compiles the IR to a real `terraform plan` against AWS via OIDC; state in S3 + DynamoDB. @@ -348,7 +348,7 @@ decisions: |----|----------|-----------|---------| | D-034 | Temporary long-lived AWS key (waiver) used once in Phase 08 to bootstrap the state backend + IAM user; rotated/deactivated immediately after | §12.5 forbids long-lived creds; the bootstrap needed one `aws iam` call before the spike user + rotated key could take over | Spike achieves real `terraform plan` against AWS without violating the locked target after bootstrap. **CLOSED 2026-07-21: root key `AKIA…ROOT-DEACTIVATED` deactivated by the user in the AWS IAM console (verified — `InvalidClientTokenId`); the spike uses the rotated `acdl-spike-runner` key per D-039. Key ID redacted in v1.2 Phase 12 (P1-1).** | | D-035 | Milestone version = `v1.1` (feature), ship tag `v1.2.0` | Real platform is a breaking reframing of the demo, but treated as the next incremental milestone per user choice; ship.md: feature milestone → next minor | Tag `v1.2.0` on milestone COMPLETE | -| D-036 | Spike picks `l1-s3` + `l2-static-asset` | Simplest real AWS resource (no IAM/network deps); smallest real `terraform plan`; proves the IR + adapter end-to-end | Spike scope fixed | +| D-036 | Spike picks `l1-s3` + `l2-static-assets` | Simplest real AWS resource (no IAM/network deps); smallest real `terraform plan`; proves the IR + adapter end-to-end | Spike scope fixed | | D-037 | Demo archived to `demo/` (not deleted) | Preserves the working v1.0 demo as intent reference; new platform layout under `platform/`, `schemas/`, `adapters/`, `terraform/`, `modules-ir/` | No churn on demo code; clean separation | | D-038 | Open decisions resolved in "accept recommendations + decide rest" mode | User-locked mode: accept architecture's stated recommendations (W1.A, W1.B, W2.A, BA.A); lead-developer decides the remaining 8 (W3.D, W3.E, BA.B, BA.C, BA.D, BA.E, BA.F, OpenTofu timing) with rationale | Architecture reaches v1.0 in Phase 07 | | D-039 | Spike-only waiver: per-run-rotated long-lived AWS key. OIDC federation deferred to v1.2, blocked on go-gitea/gitea#36988. | **RESEARCH TARGET 1 verdict (conf 0.95):** Gitea Actions does NOT support `id-token: write` / OIDC token issuance as of Gitea 1.27.x / gitea-runner v2.1.0. GitHub's OIDC pattern is not portable. The waiver satisfies §12.5's *intent* (no persistent long-lived key) for the spike: the key is rotated after each run by `scripts/rotate_spike_key.sh`. v1.2 implements real OIDC when the Gitea PR merges. | Spike achieves real `terraform plan` against AWS without a *persistently* long-lived key; real OIDC is a v1.2 deliverable | @@ -368,7 +368,7 @@ or user-directed scope). New v1.7 decisions: | ID | Decision | Rationale | Outcome | |----|----------|-----------|---------| -| D-048 | Rename `static-asset` → `static-assets`: **rewrite all occurrences** including verbatim historical phase descriptions in `.ciagent/` (ROADMAP, REQUIREMENTS, RESEARCH, decision tables), overriding the v1.6 audit precedent that preserved some historical references. | User chose full rewrite. Maximally consistent; the reconstruction test is updated to expect `static-assets` throughout. | Phase 22 rewrites every `static-asset` string to `static-assets`; no preserved historical tokens remain. | +| D-048 | Rename `static-assets` → `static-assets`: **rewrite all occurrences** including verbatim historical phase descriptions in `.ciagent/` (ROADMAP, REQUIREMENTS, RESEARCH, decision tables), overriding the v1.6 audit precedent that preserved some historical references. | User chose full rewrite. Maximally consistent; the reconstruction test is updated to expect `static-assets` throughout. | Phase 22 rewrites every `static-assets` string to `static-assets`; no preserved historical tokens remain. | | D-049 | Production static-assets stack = S3 + CloudFront (OAC) + WAF. | Self-contained, domain-free production edge. Route53/ACM are domain-dependent (consumer-supplied) and deferred to documented extension points / a complex example. | Phase 22 authors `cloudfront` + `waf` primitives and augments the module. | | D-050 | Deploy outputs: SSM Parameter Store (`SecureString`, KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values + GitHub PR comment / job summary for human-readable connection strings. | Two canonical mechanisms: SSM for resources that read at runtime; PR comment for developers. No raw secrets in logs. | Phase 25 implements `core/output_publisher.py` + two new pipeline stages. | | D-051 | Contract ingestion storage = DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via customer-managed CMK, point-in-time recovery). | Enables historical queries, impact analysis, CMDB-style application-state queries, and pattern detection via DynamoDB queries. S3 flat-file mirror deferred (DynamoDB is sufficient for v1.7). | Phase 24 defines the table + Lambda. | @@ -419,8 +419,8 @@ sign-off (autonomy = full; all within locked constraints). | OIDC IAM role | `acdl-act-runner-role` | Assumed by the act_runner via web-identity | | OIDC trust subject | `repo:continuous-intelligence/acdl:ref:refs/heads/main` (+ phase branches) | Least-privilege; refined in Phase 08 | | Spike L1 (`l1-s3`) inputs | `bucket_name: string`, `region: string` | Minimal S3 interface per §2 | -| Spike L2 (`l2-static-asset`) | thin-composition referencing `l1-s3` only; depth 1 | Smallest real plan per D-036 | -| Spike contract | `contracts/spike.yaml`: `stack: l2-static-asset`, `environment: dev`, `inputs: { bucket_name: acdl-spike-bucket, region: us-east-1 }` | One end-to-end submission (REQ-27) | +| Spike L2 (`l2-static-assets`) | thin-composition referencing `l1-s3` only; depth 1 | Smallest real plan per D-036 | +| Spike contract | `contracts/spike.yaml`: `stack: l2-static-assets`, `environment: dev`, `inputs: { bucket_name: acdl-spike-bucket, region: us-east-1 }` | One end-to-end submission (REQ-27) | | Spike `terraform` command | `plan` only | `apply` is out of scope (Out of Scope table); HITL-gated in v1.2 | | Checkov ruleset (spike) | the 4 L2 checks (secrets-in-plaintext, public ingress, IAM wildcard, KMS key reference) + tag/naming | §3 + §12.4; Kyverno/OPA deferred | | v1.0 tags preserved | `v1.0.1`..`v1.0.5`, `v1.1.0` retained | Immutability; demo archive does not rewrite history | diff --git a/.ciagent/REQUIREMENTS.md b/.ciagent/REQUIREMENTS.md index 7bb2853..546b43f 100644 --- a/.ciagent/REQUIREMENTS.md +++ b/.ciagent/REQUIREMENTS.md @@ -51,11 +51,11 @@ ### Category: v1 Spike — IR, L1, Adapter - **REQ-24:** One real L1 module `l1-s3` exists under `modules-ir/l1/l1-s3/` with an IR-typed interface (typed inputs/outputs/NFRs) registered in the L1 registry. -- **REQ-25:** One real L2 thin-composition `l2-static-asset` exists under `modules-ir/l2/l2-static-asset/` referencing `l1-s3` only (depth 1, within max-depth-5). +- **REQ-25:** One real L2 thin-composition `l2-static-assets` exists under `modules-ir/l2/l2-static-assets/` referencing `l1-s3` only (depth 1, within max-depth-5). - **REQ-26:** The Terraform adapter (`adapters/terraform/`) compiles the IR-typed L1 interface to Terraform `variable`/`output` blocks and the L2 thin-composition tree to a Terraform root module; it emits a real `terraform plan` against AWS via OIDC; state is stored in S3 + DynamoDB. ### Category: v1 Spike — End-to-End -- **REQ-27:** One end-to-end contract submission (`contracts/spike.yaml` for `l2-static-asset`) flows through: contract schema validation → contract→IR resolution → `terraform plan` (real AWS) → Checkov `PolicyCheckResult` → confidence signal → evidence event written to the DynamoDB outbox. +- **REQ-27:** One end-to-end contract submission (`contracts/spike.yaml` for `l2-static-assets`) flows through: contract schema validation → contract→IR resolution → `terraform plan` (real AWS) → Checkov `PolicyCheckResult` → confidence signal → evidence event written to the DynamoDB outbox. - **REQ-28:** Spike verification (`scripts/verify_phase10.sh`) proves the IR-shaped commitments hold: the adapter is the only substrate-specific code; no polyglot mess; the L1 content, contract YML, and thin-composition tree are substrate-agnostic. ## Out of Scope (v1.1) @@ -124,12 +124,12 @@ ### Category: Consumer Happy Path Documentation - **REQ-46:** `README.md` is rewritten so the consumer model is unambiguous: this repo is the platform source; a consumer never clones it. A consumer repo contains only app code + `contract.yaml` referencing the central pipeline + contract. The platform-flow diagram is a mermaid `flowchart TD` (replacing the ASCII art). "L3A"/"L3B" nomenclature is removed from README (single-surface model). "spike" nomenclature is removed from prose (code paths in bash blocks are kept verbatim). -- **REQ-47:** `docs/CONSUMER_GUIDE.md` (all-caps) replaces `docs/consumer-guide-static-asset.md`. It is generic across all L2 modules (`static-asset` as the worked example), uses mermaid diagrams (model + pipeline flow), documents versioned `uses:` references (floating MAJOR+MINOR tags — bare/`@main` discouraged), scopes prerequisites to consumer-repo bootstrap only (no Terraform/Checkov/boto3/runner-key — those are platform-repo concerns), and documents that the pipeline fetches the ACDL repo at run time via a reusable workflow (consumers never invoke `scripts/run_platform.sh` locally for the happy path). +- **REQ-47:** `docs/CONSUMER_GUIDE.md` (all-caps) replaces `docs/consumer-guide-static-assets.md`. It is generic across all L2 modules (`static-assets` as the worked example), uses mermaid diagrams (model + pipeline flow), documents versioned `uses:` references (floating MAJOR+MINOR tags — bare/`@main` discouraged), scopes prerequisites to consumer-repo bootstrap only (no Terraform/Checkov/boto3/runner-key — those are platform-repo concerns), and documents that the pipeline fetches the ACDL repo at run time via a reusable workflow (consumers never invoke `scripts/run_platform.sh` locally for the happy path). - **REQ-48:** `README.md` Credentials section is rewritten to express the zero-trust target model: consumer repos use OIDC federation (no long-lived keys) with attribute-based authorization (ABAC) — IAM roles + session policies scoped by repository identity and resource-creation tags so a consumer can only view/update resources it created (blast-radius containment). A documented override allows a static key in GitHub Secrets (consumer repo) or `.env.secrets` (local testing), rotated by a platform-managed scheduled pipeline on a daily cadence; when `.env.secrets` is used locally, rotating out of band is the consumer's responsibility. ### Category: Reusable Deploy Workflow - **REQ-49:** A reusable deploy workflow exists as byte-identical `.gitea/workflows/deploy.yml` (Gitea, dev) and `.github/workflows/deploy.yml` (GitHub, production), implementing the central deployment pipeline contract (`pipelines/deploy.yaml` validated against `schemas/deploy-pipeline.schema.json`). It is invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@vMAJOR.MINOR` (versioned tag). The workflow checks out the consumer repo, checks out the ACDL platform repo into the runner workspace, installs runtime deps (Python, Terraform, Checkov), and invokes `scripts/run_platform.sh` against the consumer's contract path (passed as a workflow input). OIDC is the default auth (`permissions: id-token: write`); a static-key override reads from repository secrets. -- **REQ-50:** `contracts/static-asset.yaml` uses a versioned `uses:` reference (`@v1.4`, MAJOR+MINOR) — not bare `@v1` or `@main` — as the canonical example the consumer guide points at. +- **REQ-50:** `contracts/static-assets.yaml` uses a versioned `uses:` reference (`@v1.4`, MAJOR+MINOR) — not bare `@v1` or `@main` — as the canonical example the consumer guide points at. - **REQ-51:** `tests/test_pipeline_contract.py` is extended to validate the new deploy workflows: both files exist, are byte-identical, and conform to `schemas/deploy-pipeline.schema.json` (stages present, names match `pipelines/deploy.yaml` stage names). The existing CI-workflow conformance tests continue to pass unchanged. ## v1.6 (Active — consumer-facing docs restructure + terminology normalization + environments concept) @@ -157,7 +157,7 @@ ## v1.7 (Active — production platform + contract ingestion + pipeline maturation) ### Category: Rename + production-ready stack -- **REQ-62:** `static-asset` is renamed to `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative: verbatim phase descriptions, REQ-25/27/50 text, D-036, RESEARCH.md). `grep -R "static-asset[^s]" .` (excluding `.git/`) returns 0 hits. The module dir `modules/l2/static-asset/` → `modules/l2/static-assets/`; `contracts/static-asset.yaml` → `contracts/static-assets.yaml`; the registry key is renamed; all scripts, tests, docs, and `.ciagent/` files use `static-assets`. The reconstruction test is updated to expect `static-assets` throughout. +- **REQ-62:** `static-assets` is renamed to `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative: verbatim phase descriptions, REQ-25/27/50 text, D-036, RESEARCH.md). `grep -R "static-assets[^s]" .` (excluding `.git/`) returns 0 hits. The module dir `modules/l2/static-assets/` → `modules/l2/static-assets/`; `contracts/static-assets.yaml` → `contracts/static-assets.yaml`; the registry key is renamed; all scripts, tests, docs, and `.ciagent/` files use `static-assets`. The reconstruction test is updated to expect `static-assets` throughout. - **REQ-63:** Two new primitives exist: `cloudfront` (distribution + OAC, stack types `aws:cloudfront:distribution` + `aws:cloudfront:originaccesscontrol`) and `waf` (WAFv2 web ACL, stack type `aws:wafv2:webacl`), each with an `interface.json` valid against `schemas/stack.schema.json` and a full README (Resources/Inputs/Outputs/Usage/Compliance/Versioning). Both are registered in `modules/registry.json`. The Terraform adapter `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` covers the new stack types. - **REQ-64:** The `static-assets` module is augmented to a production-ready stack referencing s3 + cloudfront + waf (depth 1, D-049). `composition.json` wires the s3 bucket regional domain name to the CloudFront origin, and the WAF web ACL ARN to the CloudFront distribution. `schemas/contract.schema.json` is extended for the new module inputs (`price_class`, `viewer_protocol_policy`, `waf_enabled`, `default_ttl`, `max_ttl`). The `uses:`/`ref:` tag advances from `@v1.4` to `@v1.6` (D-056/D-057); floating git tags `v1.6` + `v1` are created pointing at `v1.6.0`. diff --git a/.ciagent/RESEARCH.md b/.ciagent/RESEARCH.md index 44216a5..930dc27 100644 --- a/.ciagent/RESEARCH.md +++ b/.ciagent/RESEARCH.md @@ -453,7 +453,7 @@ the hooks are on the *composition*, not the resource). interpolation `module.X.`. - `relationship.kind = parent` → the child resource is *inside* the parent L1's module block (no Terraform construct; it's a composition hint the - adapter uses to order module blocks). For the spike (`l2-static-asset` → + adapter uses to order module blocks). For the spike (`l2-static-assets` → `l1-s3` only, depth 1) there is exactly one resource and zero relationships — the IR still validates, and the adapter produces a single `module "s3" { ... }` block. @@ -704,7 +704,7 @@ exists in *every* environment (including dev). | 3 | freshness | 0.10 | Age of the contract's declared validation evidence (e2eSuite, loadTest) relative to submission; in dev, this is the age of the L1/L2 module versions vs. the registry | L1 registry publication timestamps | | 4 | source / attestation | 0.15 | Identity of the submitter + the contract's source provenance (git ref, commit SHA, signed-by). In dev (autonomous), this is "any valid submitter" — the gate is *presence*, not *identity*. | Gitea `gitea.actor` + commit SHA | | 5 | historical behavior | 0.10 | Platform's observed history for this contract / stack / submitter: prior rollback count, prior policy-fail count. In the spike (first submission), this is a neutral 0.5 (no history). | DynamoDB outbox (prior events for this `contractId` / `stack`) | -| 6 | NFR conformance | 0.10 | The contract's declared NFRs (latency, throughput, error rate) vs. the platform's measured baseline for this stack. In the spike, `l2-static-asset` declares no NFRs, so this input is "present + neutral 0.5" (the gate is *presence*, not *conformance*). | contract `nfrs` block (optional) + platform baseline (none in spike) | +| 6 | NFR conformance | 0.10 | The contract's declared NFRs (latency, throughput, error rate) vs. the platform's measured baseline for this stack. In the spike, `l2-static-assets` declares no NFRs, so this input is "present + neutral 0.5" (the gate is *presence*, not *conformance*). | contract `nfrs` block (optional) + platform baseline (none in spike) | **Weights sum to 1.0.** The base score (before severity penalties) is the weighted sum of each input's per-input score (each in [0,1]). The @@ -909,12 +909,12 @@ of Object Lock + JWS is a scope decision, not a design risk. "seq": 1, "ts": "2026-07-21T12:00:00Z", "stage": "dev", - "event": "contract applied: l2-static-asset (confidence 0.82, band pass)", + "event": "contract applied: l2-static-assets (confidence 0.82, band pass)", "prev_hash": "", "hash": "", "contractId": "uuid", "environment": "dev", - "stack": "l2-static-asset", + "stack": "l2-static-assets", "score": 0.82, "band": "pass" } @@ -1116,7 +1116,7 @@ a direct formalization. **Spike contract (`contracts/spike.yaml`) validates against this:** ```yaml -stack: l2-static-asset +stack: l2-static-assets environment: dev inputs: bucket_name: acdl-spike-bucket diff --git a/.ciagent/ROADMAP.md b/.ciagent/ROADMAP.md index a533da4..1c44ec7 100644 --- a/.ciagent/ROADMAP.md +++ b/.ciagent/ROADMAP.md @@ -9,7 +9,7 @@ - **v1.4 (complete):** central pipeline contract + shell reproducibility + output streaming. A declarative pipeline contract (`schemas/pipeline.schema.json` + `pipelines/ci.yaml`) binds the Gitea and GitHub workflows to a single source of truth. `scripts/run_ci.sh` mirrors the CI pipeline locally. `scripts/run_platform.sh` streams terraform/checkov output by default. - **v1.5 (complete, tag `v1.5.0`):** consumer happy path + zero-trust docs + reusable deploy workflow. README rewritten so the consumer model is unambiguous (consumer owns only contract + app code; the rest is the platform source). Platform-flow + consumer-guide diagrams converted to mermaid. Legacy surface + implementation nomenclature removed from docs. Credentials section rewritten for zero-trust OIDC + ABAC (with a static-key override + daily rotation). A generic `docs/CONSUMER_GUIDE.md` (all L2 modules, versioned `uses:`, consumer-scoped prereqs, run-time platform fetch) replaces the module-specific guide. A byte-identical reusable `deploy.yml` workflow (Gitea + GitHub) implements `pipelines/deploy.yaml` and is invoked by consumer repos via a versioned tag. - **v1.6 (complete, tag `v1.6.0`):** consumer-facing docs restructure + terminology normalization + environments concept. `docs/` becomes a Jekyll-style GitHub Pages site. `acdl_platform/` is renamed to `core/`. L2 → "modules", L1 → "primitives", "composition" → "pattern" in prose. README restructured: Features + Roadmap (no internal status), repository roles restated (consumer = app code + contracts + CI definitions), mermaid fixed (visible text, security-checks + infrastructure-apply stages, no tool names), credentials section minus go-gitea/waivers. Platform-managed environments concept + a minimal onboarding scaffold. `.ciagent/` + `.gitea/` references removed from all consumer-facing docs. -- **v1.7 (active, tag `v1.7.0`):** production platform + contract ingestion + pipeline maturation. Rename `static-asset` → `static-assets` (D-048 — incl. `.ciagent/` historical narrative). Author `cloudfront` + `waf` primitives; augment `static-assets` to a production-ready S3 + CloudFront (OAC) + WAF stack (D-049). Tagging-standard enforcement (Checkov custom rule, D-043 closure, D-054). Wiz adapter stub (D-052) + Kyverno K8s-native adapter (D-053). Platform Lambda + DynamoDB `acdl-contracts` table for contract ingestion (D-051) + cross-account IAM. Deploy outputs via SSM SecureString + GitHub PR comment (D-050). Uniform error reporting via the Lambda `report_error` action → GitHub issue on the platform repo (D-055); Gitea excluded. Stage comments after every successful pipeline stage. Three platform pipelines (platform-test unit+integration, primitives-plan, patterns-plan). Release job with semver + MAJOR.MINOR/MAJOR tag maintenance (D-057). `uses:`/`ref:` bumped to `@v1.6`; floating `v1.6` + `v1` tags created in Phase 22. Remove `consumer-repos/`; add validated per-module examples (`modules//examples/`, D-058) including a new RDS primitive demonstrating multi-engine variation (D-059). +- **v1.7 (active, tag `v1.7.0`):** production platform + contract ingestion + pipeline maturation. Rename `static-assets` → `static-assets` (D-048 — incl. `.ciagent/` historical narrative). Author `cloudfront` + `waf` primitives; augment `static-assets` to a production-ready S3 + CloudFront (OAC) + WAF stack (D-049). Tagging-standard enforcement (Checkov custom rule, D-043 closure, D-054). Wiz adapter stub (D-052) + Kyverno K8s-native adapter (D-053). Platform Lambda + DynamoDB `acdl-contracts` table for contract ingestion (D-051) + cross-account IAM. Deploy outputs via SSM SecureString + GitHub PR comment (D-050). Uniform error reporting via the Lambda `report_error` action → GitHub issue on the platform repo (D-055); Gitea excluded. Stage comments after every successful pipeline stage. Three platform pipelines (platform-test unit+integration, primitives-plan, patterns-plan). Release job with semver + MAJOR.MINOR/MAJOR tag maintenance (D-057). `uses:`/`ref:` bumped to `@v1.6`; floating `v1.6` + `v1` tags created in Phase 22. Remove `consumer-repos/`; add validated per-module examples (`modules//examples/`, D-058) including a new RDS primitive demonstrating multi-engine variation (D-059). - **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html --- @@ -130,12 +130,12 @@ D-034 closed (root key deactivated by user).** - `terraform validate` + `terraform plan` succeed; no long-lived credential in the workflow. ### Phase 10 — v1-spike-l2-and-contract-e2e -- **Description:** Implement `l2-static-asset` (thin-composition referencing `l1-s3`), the contract schema + contract→IR resolution, and one end-to-end contract submission (`contracts/spike.yaml` for `l2-static-asset`) flowing through schema validation → IR resolution → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB outbox. Verify the IR commitments hold (no polyglot mess). +- **Description:** Implement `l2-static-assets` (thin-composition referencing `l1-s3`), the contract schema + contract→IR resolution, and one end-to-end contract submission (`contracts/spike.yaml` for `l2-static-assets`) flowing through schema validation → IR resolution → `terraform plan` → Checkov `PolicyCheckResult` → confidence signal → evidence event to the DynamoDB outbox. Verify the IR commitments hold (no polyglot mess). - **Status:** complete (v1.1.5) - **Depends on:** [09] - **Requirements:** REQ-25, REQ-27, REQ-28 - **Success Criteria:** - - `l2-static-asset` references `l1-s3` only (depth 1). + - `l2-static-assets` references `l1-s3` only (depth 1). - One contract submission completes the full pipeline end-to-end. - `scripts/verify_phase10.sh` proves the adapter is the only substrate-specific code. - Evidence event is written to the DynamoDB outbox. @@ -292,16 +292,16 @@ consumer repos never need to clone the platform repo or invoke its scripts locally. ### Phase 20 — consumer-happy-path-and-reusable-deploy-workflow -- **Description:** Rewrite `README.md` so the consumer model is unambiguous (this repo is the platform source; a consumer owns only `contract.yaml` + app code). Convert the platform-flow diagram to a mermaid `flowchart TD`. Remove "L3A"/"L3B" + "spike" nomenclature from README prose. Rewrite the Credentials section for zero-trust OIDC + ABAC (with a static-key override + daily rotation; consumer rotates out of band when using `.env.secrets` locally). Replace `docs/consumer-guide-static-asset.md` with a generic `docs/CONSUMER_GUIDE.md` (all L2 modules, mermaid diagrams, versioned `uses:` floating MAJOR+MINOR, consumer-scoped prerequisites, run-time platform fetch via a reusable workflow). Create byte-identical `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` implementing `pipelines/deploy.yaml` — a reusable workflow invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@v1.4` that checks out the consumer repo + the ACDL platform repo and runs `scripts/run_platform.sh`. Update `contracts/static-asset.yaml` to `uses: acdl/pipelines/deploy.yaml@v1.4`. Extend `tests/test_pipeline_contract.py` to validate the new deploy workflows (byte-identical, schema-conformant). +- **Description:** Rewrite `README.md` so the consumer model is unambiguous (this repo is the platform source; a consumer owns only `contract.yaml` + app code). Convert the platform-flow diagram to a mermaid `flowchart TD`. Remove "L3A"/"L3B" + "spike" nomenclature from README prose. Rewrite the Credentials section for zero-trust OIDC + ABAC (with a static-key override + daily rotation; consumer rotates out of band when using `.env.secrets` locally). Replace `docs/consumer-guide-static-assets.md` with a generic `docs/CONSUMER_GUIDE.md` (all L2 modules, mermaid diagrams, versioned `uses:` floating MAJOR+MINOR, consumer-scoped prerequisites, run-time platform fetch via a reusable workflow). Create byte-identical `.gitea/workflows/deploy.yml` + `.github/workflows/deploy.yml` implementing `pipelines/deploy.yaml` — a reusable workflow invoked by consumer repos via `uses: acdl/.gitea/workflows/deploy.yml@v1.4` that checks out the consumer repo + the ACDL platform repo and runs `scripts/run_platform.sh`. Update `contracts/static-assets.yaml` to `uses: acdl/pipelines/deploy.yaml@v1.4`. Extend `tests/test_pipeline_contract.py` to validate the new deploy workflows (byte-identical, schema-conformant). - **Status:** complete (v1.5.0) - **Depends on:** [19] - **Requirements:** REQ-46, REQ-47, REQ-48, REQ-49, REQ-50, REQ-51 - **Success Criteria:** - `README.md` states the platform-source vs consumer-repo distinction up front; platform flow is a mermaid `flowchart TD`; `grep L3B README.md` returns 0 hits; `grep -i spike README.md` returns 0 prose hits (code paths in bash blocks allowed). - - `docs/CONSUMER_GUIDE.md` exists; `docs/consumer-guide-static-asset.md` is deleted; `grep -R consumer-guide-static-asset` returns 0 dangling references; guide is generic (static-asset is the worked example, not the scope); diagrams are mermaid; `uses:` references use `@v1.4`. + - `docs/CONSUMER_GUIDE.md` exists; `docs/consumer-guide-static-assets.md` is deleted; `grep -R consumer-guide-static-assets` returns 0 dangling references; guide is generic (static-assets is the worked example, not the scope); diagrams are mermaid; `uses:` references use `@v1.4`. - `README.md` Credentials section describes OIDC + ABAC zero-trust as the default and the static-key override + daily rotation + consumer out-of-band rotation duty for local `.env.secrets`. - `.gitea/workflows/deploy.yml` and `.github/workflows/deploy.yml` exist, are byte-identical, conform to `schemas/deploy-pipeline.schema.json`, and are reusable (`on: workflow_call` with a `contract` input). - - `contracts/static-asset.yaml` uses `uses: acdl/pipelines/deploy.yaml@v1.4`. + - `contracts/static-assets.yaml` uses `uses: acdl/pipelines/deploy.yaml@v1.4`. - `tests/test_pipeline_contract.py` validates the deploy workflows (exist, byte-identical, schema-conformant); the extended test suite passes; `bash scripts/run_ci.sh` exits 0. After Phase 20: COMPLETE gate — review → ship `v1.5.0` → audit. @@ -355,12 +355,12 @@ The `uses:`/`ref:` tag advances from `@v1.4` to `@v1.6`; the floating `v1.6` + reference is never broken, and the release job (Phase 26) owns ongoing updates. ### Phase 22 — rename-and-production-static-assets-stack -- **Description:** Rename `static-asset` → `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative, overriding the v1.6 preservation precedent). Author two new primitives: `cloudfront` (distribution + OAC, stack types `aws:cloudfront:distribution` + `aws:cloudfront:originaccesscontrol`) and `waf` (WAFv2 web ACL, stack type `aws:wafv2:webacl`). Augment the `static-assets` module to a production-ready stack referencing s3 + cloudfront + waf (depth 1, D-049). Expand the Terraform adapter `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` for the new stack types. Bump `uses:`/`ref:` from `@v1.4` to `@v1.6` (D-056/D-057); create the floating `v1.6` + `v1` git tags pointing at `v1.6.0` so the reference resolves immediately. +- **Description:** Rename `static-assets` → `static-assets` everywhere (D-048 — including `.ciagent/` historical narrative, overriding the v1.6 preservation precedent). Author two new primitives: `cloudfront` (distribution + OAC, stack types `aws:cloudfront:distribution` + `aws:cloudfront:originaccesscontrol`) and `waf` (WAFv2 web ACL, stack type `aws:wafv2:webacl`). Augment the `static-assets` module to a production-ready stack referencing s3 + cloudfront + waf (depth 1, D-049). Expand the Terraform adapter `TYPE_MAP`/`INPUT_MAP`/`OUTPUT_MAP` for the new stack types. Bump `uses:`/`ref:` from `@v1.4` to `@v1.6` (D-056/D-057); create the floating `v1.6` + `v1` git tags pointing at `v1.6.0` so the reference resolves immediately. - **Status:** active - **Depends on:** [21] - **Requirements:** REQ-62, REQ-63, REQ-64 - **Success Criteria:** - - `grep -R "static-asset[^s]" .` (excluding `.git/`) returns 0 hits; `modules/l2/static-asset/` is renamed to `modules/l2/static-assets/`; `contracts/static-asset.yaml` → `contracts/static-assets.yaml`; registry key renamed; all `.ciagent/` references (incl. verbatim phase descriptions, REQ-25/27/50 text, D-036) rewritten to `static-assets`. + - `grep -R "static-assets[^s]" .` (excluding `.git/`) returns 0 hits; `modules/l2/static-assets/` is renamed to `modules/l2/static-assets/`; `contracts/static-assets.yaml` → `contracts/static-assets.yaml`; registry key renamed; all `.ciagent/` references (incl. verbatim phase descriptions, REQ-25/27/50 text, D-036) rewritten to `static-assets`. - `modules/l1/cloudfront/` + `modules/l1/waf/` exist with `interface.json` valid against `schemas/stack.schema.json`; registered in `modules/registry.json`. - `modules/l2/static-assets/composition.json` references s3 + cloudfront + waf (depth 1). - `adapters/terraform/adapter.py` `TYPE_MAP` covers `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`, `aws:wafv2:webacl`. diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 9035c68..0b565d7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -8,8 +8,8 @@ # declared difference is the forge/runtime, not the stages or commands. # # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): -# uses: acdl/.gitea/workflows/deploy.yml@v1.4 (Gitea) -# uses: acdl/.github/workflows/deploy.yml@v1.4 (GitHub) +# uses: acdl/.gitea/workflows/deploy.yml@v1.6 (Gitea) +# uses: acdl/.github/workflows/deploy.yml@v1.6 (GitHub) # # Unversioned references (@main, bare) are discouraged — the consumer's setup # must be immutable + resilient. The versioned tag is the only immutability @@ -74,7 +74,7 @@ jobs: with: repository: acdl/acdl path: platform - ref: v1.4 + ref: v1.6 - uses: actions/setup-python@v5 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9035c68..0b565d7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,8 +8,8 @@ # declared difference is the forge/runtime, not the stages or commands. # # Consumer repos invoke this workflow via a versioned tag (floating MAJOR + MINOR): -# uses: acdl/.gitea/workflows/deploy.yml@v1.4 (Gitea) -# uses: acdl/.github/workflows/deploy.yml@v1.4 (GitHub) +# uses: acdl/.gitea/workflows/deploy.yml@v1.6 (Gitea) +# uses: acdl/.github/workflows/deploy.yml@v1.6 (GitHub) # # Unversioned references (@main, bare) are discouraged — the consumer's setup # must be immutable + resilient. The versioned tag is the only immutability @@ -74,7 +74,7 @@ jobs: with: repository: acdl/acdl path: platform - ref: v1.4 + ref: v1.6 - uses: actions/setup-python@v5 with: diff --git a/README.md b/README.md index 563de91..de69e8f 100644 --- a/README.md +++ b/README.md @@ -155,15 +155,15 @@ ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID=... ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY=... \ # 3. Run the full platform pipeline (contract -> environment check -> stack -> # adapter -> security checks -> infrastructure plan -> policy checks -> # confidence -> evidence event -> apply). Output is streamed to stdout. -bash scripts/run_platform.sh contracts/static-asset.yaml +bash scripts/run_platform.sh contracts/static-assets.yaml # Expected: "=== PLATFORM E2E OK ===" # Or plan-only (contract -> stack -> adapter -> infrastructure plan; no # policy checks / outbox): -bash scripts/run_platform.sh --plan-only contracts/static-asset.yaml +bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml # Add --quiet to suppress streaming (output to log files only): -bash scripts/run_platform.sh --quiet contracts/static-asset.yaml +bash scripts/run_platform.sh --quiet contracts/static-assets.yaml ``` ### Test the platform (offline, no AWS required) @@ -176,7 +176,7 @@ pip install -r requirements-test.txt python3 -m pytest tests/ -v # Run the platform in check-only mode (offline — no AWS, no policy checks, -# no outbox). Uses the default sample contract (contracts/static-asset.yaml) +# no outbox). Uses the default sample contract (contracts/static-assets.yaml) # and the sample dev environment (core/environments/dev.json). bash scripts/run_platform.sh --check-only # Expected: "=== PLATFORM CHECK OK ===" @@ -222,7 +222,7 @@ The workflow implements the same stages as `pipelines/deploy.yaml` (validate-contract → resolve-stack → security checks → infrastructure plan → policy checks → confidence → evidence event → apply). A consumer repo invokes the reusable workflow via a **versioned tag** (floating MAJOR + -MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.4`). The workflow checks +MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`). The workflow checks out the consumer repo, then checks out the ACDL platform repo into the runner workspace, and runs `scripts/run_platform.sh` against the consumer's contract — the consumer never clones the platform repo or invokes its @@ -249,7 +249,7 @@ backwards-compatible log-only mode. A step-by-step guide for a consumer to create their pipeline and define a contract that deploys any ACDL module to AWS is at [`docs/consumer-guide.md`](docs/consumer-guide.md). The guide is generic -across all modules; `static-asset` is the worked example. +across all modules; `static-assets` is the worked example. ## Repository layout @@ -260,8 +260,8 @@ across all modules; `static-asset` is the worked example. | `pipelines/` | Central pipeline contracts: `ci.yaml` (CI), `deploy.yaml` (deployment) | active | | `adapters/` | Substrate adapters — the substrate adapter (the only substrate-specific code per §12) + the policy adapter | active | | `terraform/` | State backend (S3 + DynamoDB) + platform TF (`terraform/spike/`) + bootstrap scripts (`terraform/bootstrap/`) | active | -| `modules/` | Primitives + modules + `registry.json`. Primitives: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr. Modules: microservice, static-asset | active | -| `contracts/` | Sample consumer contracts (e.g. `static-asset.yaml`) | active | +| `modules/` | Primitives + modules + `registry.json`. Primitives: s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf. Modules: microservice, static-assets | active | +| `contracts/` | Sample consumer contracts (e.g. `static-assets.yaml`) | active | | `scripts/` | Platform run script (`run_platform.sh` with `--check-only`/`--plan-only`/`--quiet`), CI pipeline script (`run_ci.sh`), key rotation | active | | `tests/` | Pytest suite (all offline — adapter, confidence signal, policy adapter, outbox writer, pipeline contract, contract resolver, streaming, environment check) | active | | `.github/workflows/` | GitHub Actions workflows: `ci.yml` (CI), `deploy.yml` (reusable deploy, invoked by consumer repos) | active | diff --git a/adapters/terraform/adapter.py b/adapters/terraform/adapter.py index 8d3d7af..5397e38 100644 --- a/adapters/terraform/adapter.py +++ b/adapters/terraform/adapter.py @@ -36,6 +36,9 @@ TYPE_MAP = { "aws:elbv2:listener": "aws_lb_listener", "aws:elbv2:targetgroup": "aws_lb_target_group", "aws:ecr:repository": "aws_ecr_repository", + "aws:cloudfront:distribution": "aws_cloudfront_distribution", + "aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control", + "aws:wafv2:webacl": "aws_wafv2_web_acl", } # Stack input name -> Terraform arg name, per stack type. Only non-identity @@ -54,6 +57,9 @@ INPUT_MAP = { "aws:elbv2:listener": {}, "aws:elbv2:targetgroup": {"port": "port", "protocol": "protocol"}, "aws:ecr:repository": {}, + "aws:cloudfront:distribution": {"bucket_regional_domain_name": "origin_domain_name", "price_class": "price_class", "viewer_protocol_policy": "viewer_protocol_policy", "default_ttl": "default_ttl", "max_ttl": "max_ttl", "waf_web_acl_arn": "web_acl_id"}, + "aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"}, + "aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"}, } # Stack output name -> Terraform attribute name, per stack type. Only @@ -72,6 +78,9 @@ OUTPUT_MAP = { "aws:elbv2:listener": {"listener_arn": "id"}, "aws:elbv2:targetgroup": {"target_group_arn": "arn"}, "aws:ecr:repository": {"repository_arn": "arn"}, + "aws:cloudfront:distribution": {"distribution_arn": "arn", "distribution_domain_name": "domain_name", "oac_id": "origin_access_control_id"}, + "aws:cloudfront:originaccesscontrol": {"oac_id": "id"}, + "aws:wafv2:webacl": {"web_acl_arn": "arn"}, } @@ -185,6 +194,23 @@ def _emit_resource(resource, type_by_id=None): if rtype == "aws:ecs:service" and in_name in ("subnets", "security_group"): # Collected into network_configuration block (emitted after all inputs). continue + if rtype == "aws:cloudfront:distribution" and in_name in ( + "bucket_regional_domain_name", "price_class", "viewer_protocol_policy", + "default_ttl", "max_ttl", "waf_web_acl_arn", "oac_id", + ): + # Collected into the origin/default_cache_behavior/web_acl_id blocks + # emitted after all inputs. + continue + if rtype == "aws:cloudfront:originaccesscontrol" and in_name in ( + "name", "origin_type", "signing_behavior", + ): + # Defaults emitted after all inputs. + continue + if rtype == "aws:wafv2:webacl" and in_name in ( + "name", "scope", "default_action", "rules", + ): + # Structured blocks emitted after all inputs. + continue body.append(f"{arg} = {_value_expr(value, type_by_id)}") if rtype == "aws:ecs:service": subnets_val = inputs.get("subnets") @@ -246,6 +272,96 @@ def _emit_resource(resource, type_by_id=None): body.append("tags = {") body.append(' Name = "acdl-microservice-rt"') body.append("}") + if rtype == "aws:cloudfront:originaccesscontrol": + name = inputs.get("name", "acdl-oac") + if isinstance(name, str) and name.startswith("ref:"): + name = _ref_expr(name, type_by_id) + else: + name = _tf_value(name) + body.append(f"name = {name}") + body.append("origin_access_control_origin_type = \"s3\"") + body.append("origin_access_control_signing_behavior = \"always\"") + if rtype == "aws:cloudfront:distribution": + origin_domain = inputs.get("bucket_regional_domain_name") + if isinstance(origin_domain, str) and origin_domain.startswith("ref:"): + origin_domain = _ref_expr(origin_domain, type_by_id) + else: + origin_domain = _tf_value(origin_domain) + # The OAC resource id follows the convention "-originaccesscontrol"; + # derive it from this distribution's id. + if rid.endswith("-distribution"): + oac_rid = rid[: -len("distribution")] + "originaccesscontrol" + else: + oac_rid = "cloudfront-originaccesscontrol" + body.append("origin {") + body.append(f" domain_name = {origin_domain}") + body.append(f" origin_access_control = aws_cloudfront_origin_access_control.{oac_rid}.id") + body.append(" s3_origin_config {}") + body.append("}") + body.append("enabled = true") + price_class = inputs.get("price_class", "PriceClass_100") + vpp = inputs.get("viewer_protocol_policy", "redirect-to-https") + default_ttl = inputs.get("default_ttl", 3600) + max_ttl = inputs.get("max_ttl", 86400) + body.append("default_cache_behavior {") + body.append(f" viewer_protocol_policy = {_value_expr(vpp, type_by_id)}") + body.append(f" target_origin_id = {_tf_value(rid)}") + body.append(" min_ttl = 0") + body.append(f" default_ttl = {_value_expr(default_ttl, type_by_id)}") + body.append(f" max_ttl = {_value_expr(max_ttl, type_by_id)}") + body.append(" allowed_methods = [\"GET\", \"HEAD\"]") + body.append(" cached_methods = [\"GET\", \"HEAD\"]") + body.append("}") + body.append(f"price_class = {_value_expr(price_class, type_by_id)}") + body.append("restrictions {") + body.append(" geo_restriction {") + body.append(" restriction_type = \"none\"") + body.append(" }") + body.append("}") + body.append("viewer_certificate {") + body.append(" cloudfront_default_certificate = true") + body.append("}") + waf_arn = inputs.get("waf_web_acl_arn") + if waf_arn is not None: + if isinstance(waf_arn, str) and waf_arn.startswith("ref:"): + waf_expr = _ref_expr(waf_arn, type_by_id) + else: + waf_expr = _tf_value(waf_arn) + body.append(f"web_acl_id = {waf_expr}") + if rtype == "aws:wafv2:webacl": + name = inputs.get("name", "acdl-waf") + body.append(f"name = {_tf_value(name) if not isinstance(name, str) or not name.startswith('ref:') else _ref_expr(name, type_by_id)}") + body.append("scope = \"cloudfront\"") + body.append("default_action {") + body.append(" allow {}") + body.append("}") + body.append("visibility_config {") + body.append(" cloudwatch_metrics_enabled = true") + body.append(" metric_name = \"acdl-waf-metrics\"") + body.append(" sampled_requests_enabled = true") + body.append("}") + rules_input = inputs.get("rules") + if rules_input: + body.append(f"rules = {_value_expr(rules_input, type_by_id)}") + else: + body.append("rules {") + body.append(" name = \"aws-managed-rules\"") + body.append(" priority = 0") + body.append(" override_action {") + body.append(" none {}") + body.append(" }") + body.append(" statement {") + body.append(" managed_rule_group_statement {") + body.append(" name = \"AWSManagedRulesCommonRuleSet\"") + body.append(" vendor_name = \"AWS\"") + body.append(" }") + body.append(" }") + body.append(" visibility_config {") + body.append(" cloudwatch_metrics_enabled = true") + body.append(" metric_name = \"aws-managed-rules-metrics\"") + body.append(" sampled_requests_enabled = true") + body.append(" }") + body.append("}") return _resource_block(rid, tf_type, body) diff --git a/contracts/static-asset.yaml b/contracts/static-assets.yaml similarity index 77% rename from contracts/static-asset.yaml rename to contracts/static-assets.yaml index 1650702..f5dc0a0 100644 --- a/contracts/static-asset.yaml +++ b/contracts/static-assets.yaml @@ -1,4 +1,4 @@ -# ACDL sample consumer contract — static-asset module (dev) +# ACDL sample consumer contract — static-assets module (dev) # # This is the reference example for a consumer contract. It declares: # uses: the central ACDL deployment pipeline to reference @@ -9,8 +9,8 @@ # Validated against schemas/contract.schema.json. # Resolved by core/contract_resolver.py to a Target Stack instance. -uses: acdl/pipelines/deploy.yaml@v1.4 -module: static-asset +uses: acdl/pipelines/deploy.yaml@v1.6 +module: static-assets environment: dev inputs: bucket_name: acdl-spike-bucket diff --git a/docs/architecture.md b/docs/architecture.md index f2df1bf..4c9a96b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ The four layers: cluster). A primitive does not reference other primitives; it takes its environment as input. 2. **Modules** — patterns that combine primitives into deployable - infrastructure shapes (an ECS Fargate microservice, a static-asset site). + infrastructure shapes (an ECS Fargate microservice, a static-assets site). A module references registered primitives (max depth 5). 3. **Developer surface** — the developer-owned workflow file + contract. The developer references the central pipeline via a versioned tag and owns @@ -230,7 +230,7 @@ change to the modules/stack/confidence/audit. - A MAJOR bump requires a new registry entry (immutable publication); the old entry enters a 12-month deprecation window. - The central deploy pipeline is referenced by a floating MAJOR + MINOR tag - (e.g. `@v1.4`); patch fixes flow within the tag, breaking changes land + (e.g. `@v1.6`); patch fixes flow within the tag, breaking changes land under the next MINOR tag. See [Versioning](pipeline/versioning) for the consumer-facing details. diff --git a/docs/consumer-guide.md b/docs/consumer-guide.md index ddd9665..f295e1f 100644 --- a/docs/consumer-guide.md +++ b/docs/consumer-guide.md @@ -2,7 +2,7 @@ This guide walks a consumer through creating their pipeline and defining a contract that deploys any ACDL module to AWS. It is **generic** across all -modules in the registry; `static-asset` is the worked example, but every +modules in the registry; `static-assets` is the worked example, but every step applies to `microservice` and any future module. ## The model @@ -19,7 +19,7 @@ definitions. ```mermaid flowchart LR - A["your repo
(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.4| B + A["your repo
(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.6| B B["platform runners
(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter
-> security checks -> infrastructure plan -> policy checks
-> confidence -> apply -> evidence event| C C["your resources in AWS"] ``` @@ -27,7 +27,7 @@ flowchart LR ## Versioning the `uses:` reference The central deployment pipeline is **always versioned with floating MAJOR -and MINOR tags** (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Version +and MINOR tags** (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Version constraints cannot be expressed inside the contract, so the tag in `uses:` is the only immutability lever a consumer has. See [Versioning](pipeline/versioning) for the full rationale. @@ -47,7 +47,7 @@ platform-managed. See [Environments](environments/). environment is bound, your first pipeline run emits a friendly onboarding prompt. See [Environments](environments/). - **Authorization to reference the central pipeline.** Onboarding grants - your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.4`. + your repo the right to `uses: acdl/.github/workflows/deploy.yml@v1.6`. Contact the platform team if you have not been onboarded. ## Step 1 — Create a consumer repo @@ -92,7 +92,7 @@ In your contract YAML, declare `uses:` pointing at the central ACDL deployment pipeline with a **versioned tag** (floating MAJOR + MINOR): ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 ``` This tells the platform to run the standard deployment pipeline: @@ -101,11 +101,11 @@ policy checks → confidence → evidence event → apply. ## Step 3 — Define the contract -Write `.acdl/contract.yaml`. The `static-asset` example: +Write `.acdl/contract.yaml`. The `static-assets` example: ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 -module: static-asset +uses: acdl/pipelines/deploy.yaml@v1.6 +module: static-assets environment: dev inputs: bucket_name: my-static-site-assets @@ -115,7 +115,7 @@ inputs: A `microservice` example: ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 module: microservice environment: dev inputs: @@ -129,8 +129,8 @@ inputs: | Field | Type | Required | Description | |-------|------|----------|-------------| -| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). | -| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-asset`, `microservice`, `s3`). See the [module catalog](modules/). | +| `uses` | string | yes | Reference to the central deployment pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Bare or `@main` references are discouraged. See [Versioning](pipeline/versioning). | +| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](modules/). | | `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](environments/). | | `inputs` | object | yes | Module-specific inputs (see the module's README). | @@ -163,14 +163,14 @@ on: branches: [main] jobs: deploy: - uses: acdl/.github/workflows/deploy.yml@v1.4 + uses: acdl/.github/workflows/deploy.yml@v1.6 with: contract: .acdl/contract.yaml ``` That is the entire consumer-side workflow. When you push to `main`: -1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.4` +1. The platform runner resolves `uses: acdl/.github/workflows/deploy.yml@v1.6` to the reusable workflow **at the pinned tag**. 2. A **platform-provided runner** checks out **your** repo. 3. The runner checks out the **ACDL platform repo** into the workspace — @@ -245,22 +245,26 @@ flowchart TD After a successful `dev` run, the resources declared by your module's pattern exist in your AWS account, and an evidence event is recorded. -For the `static-asset` example: +For the `static-assets` example: - **An S3 bucket** named `my-static-site-assets` in `us-east-1` with versioning enabled. +- **A CloudFront distribution** with the S3 bucket as the origin (via + Origin Access Control) and HTTPS redirection. +- **A WAFv2 Web ACL** (CloudFront-scoped) associated with the + distribution. - **An evidence event** in the audit outbox with the contract ID, stack - name (`static-asset`), confidence score, and band. + name (`static-assets`), confidence score, and band. - **A confidence band** of `pass` (score ≥ 0.50 for dev). For other modules, consult the module's README (`modules/l1//README.md` or `modules/l2//README.md`) for the exact resources created. -## Step 7 — Upload your content (static-asset example) +## Step 7 — Upload your content (static-assets example) The platform provisions the infrastructure; you upload your content. For -the `static-asset` module: +the `static-assets` module: ```bash aws s3 sync ./assets s3://my-static-site-assets/ --acl public-read @@ -274,7 +278,7 @@ push your container image to the ECR repo the platform created. Change `environment` in your contract (keeping the same versioned `uses:`): ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 environment: qa # QA attestation + confidence >= 0.75 ``` @@ -303,7 +307,7 @@ per-module extension points. Common examples: | 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/) | All primitives and modules. | -| Sample contract | `contracts/static-asset.yaml` | The reference example contract (uses `@v1.4`). | +| Sample contract | `contracts/static-assets.yaml` | The reference example contract (uses `@v1.6`). | | Contract resolver | `core/contract_resolver.py` | Resolves contracts to stack instances. | | Substrate adapter | `adapters/terraform/adapter.py` | Compiles stack instances to infrastructure. | | Platform pipeline runner | `scripts/run_platform.sh` | The pipeline runner (platform-side; consumers do not invoke it directly). | diff --git a/docs/contracts/index.md b/docs/contracts/index.md index 840002d..c09941b 100644 --- a/docs/contracts/index.md +++ b/docs/contracts/index.md @@ -8,11 +8,11 @@ resolves, and deploys it. ## The contract file A consumer repo keeps its contract at `.acdl/contract.yaml`. A minimal -example (the `static-asset` module): +example (the `static-assets` module): ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 -module: static-asset +uses: acdl/pipelines/deploy.yaml@v1.6 +module: static-assets environment: dev inputs: bucket_name: my-static-site-assets @@ -22,7 +22,7 @@ inputs: A `microservice` example: ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 module: microservice environment: dev inputs: @@ -36,8 +36,8 @@ inputs: | Field | Type | Required | Description | |-------|------|----------|-------------| -| `uses` | string | yes | Reference to the central deploy pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.4`). Bare or `@main` references are discouraged. See [Versioning](../pipeline/versioning). | -| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-asset`, `microservice`, `s3`). See the [module catalog](../modules/). | +| `uses` | string | yes | Reference to the central deploy pipeline, **versioned** with a floating MAJOR+MINOR tag (e.g. `acdl/pipelines/deploy.yaml@v1.6`). Bare or `@main` references are discouraged. See [Versioning](../pipeline/versioning). | +| `module` | string | yes | Module name from the registry — any primitive or module (e.g. `static-assets`, `microservice`, `s3`). See the [module catalog](../modules/). | | `environment` | string | yes | The platform-managed environment to deploy to (e.g. `dev`). See [Environments](../environments/). | | `inputs` | object | yes | Module-specific inputs (see the module's README). | @@ -51,8 +51,8 @@ validate-contract stage with a clear error. ## Sample contract The reference example is -[`contracts/static-asset.yaml`](https://github.com/acdl/acdl/blob/main/contracts/static-asset.yaml), -which uses `@v1.4` as the canonical versioned `uses:` reference. +[`contracts/static-assets.yaml`](https://github.com/acdl/acdl/blob/main/contracts/static-assets.yaml), +which uses `@v1.6` as the canonical versioned `uses:` reference. ## Multiple contracts diff --git a/docs/modules/index.md b/docs/modules/index.md index fcf74b7..e0821e1 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -23,12 +23,14 @@ module's README documents which resources it creates. | `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [modules/l1/iam-role/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/iam-role/README.md) | | `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [modules/l1/alb/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/alb/README.md) | | `ecr` | `aws_ecr_repository` — ECR container image repository | [modules/l1/ecr/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/ecr/README.md) | +| `cloudfront` | `aws_cloudfront_distribution` + `aws_cloudfront_origin_access_control` — CloudFront distribution with S3 origin via OAC | [modules/l1/cloudfront/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/cloudfront/README.md) | +| `waf` | `aws_wafv2_web_acl` — WAFv2 Web ACL (CloudFront-scoped) | [modules/l1/waf/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/waf/README.md) | ## Modules | Module | What it references | Source | |--------|--------------------|--------| -| `static-asset` | 1 primitive (s3) — a static-asset S3 bucket | [modules/l2/static-asset/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/static-asset/README.md) | +| `static-assets` | 3 primitives (s3, cloudfront, waf) — a production static asset stack | [modules/l2/static-assets/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/static-assets/README.md) | | `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) — an ECS Fargate microservice | [modules/l2/microservice/README.md](https://github.com/acdl/acdl/blob/main/modules/l2/microservice/README.md) | ## Registry diff --git a/docs/pipeline/index.md b/docs/pipeline/index.md index 5c7ed6d..8d545fb 100644 --- a/docs/pipeline/index.md +++ b/docs/pipeline/index.md @@ -39,7 +39,7 @@ It is exposed to consumer repos as a **reusable workflow**: - `.github/workflows/deploy.yml` — GitHub Actions (production) A consumer repo invokes the reusable workflow via a **versioned tag** -(floating MAJOR + MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.4`). +(floating MAJOR + MINOR, e.g. `acdl/.github/workflows/deploy.yml@v1.6`). The workflow checks out the consumer repo, then checks out the ACDL platform repo into the runner workspace, and runs `scripts/run_platform.sh` against the consumer's contract. The consumer never clones the platform repo or diff --git a/docs/pipeline/versioning.md b/docs/pipeline/versioning.md index 1e7330a..02d1ca7 100644 --- a/docs/pipeline/versioning.md +++ b/docs/pipeline/versioning.md @@ -24,7 +24,7 @@ The central deploy pipeline is referenced by a **floating MAJOR + MINOR tag** in a consumer's contract and CI definition: ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 ``` Version constraints cannot be expressed inside the contract, so the tag in diff --git a/modules/README.md b/modules/README.md index 7616e1b..334356e 100644 --- a/modules/README.md +++ b/modules/README.md @@ -31,13 +31,15 @@ resources it creates. | `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/iam-role/README.md) | | `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/alb/README.md) | | `ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/ecr/README.md) | +| `cloudfront` | `aws_cloudfront_distribution` + `aws_cloudfront_origin_access_control` — CloudFront distribution with S3 origin via OAC | [README](l1/cloudfront/README.md) | +| `waf` | `aws_wafv2_web_acl` — WAFv2 Web ACL (CloudFront-scoped) | [README](l1/waf/README.md) | ## Modules | Module | What it references | README | |--------|--------------------|--------| | `microservice` | 6 primitives (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) | -| `static-asset` | 1 primitive (s3) | [README](l2/static-asset/README.md) | +| `static-assets` | 3 primitives (s3, cloudfront, waf) | [README](l2/static-assets/README.md) | ## Registry diff --git a/modules/l1/cloudfront/README.md b/modules/l1/cloudfront/README.md new file mode 100644 index 0000000..8ace2b3 --- /dev/null +++ b/modules/l1/cloudfront/README.md @@ -0,0 +1,78 @@ +# cloudfront — CloudFront distribution + +> **Module kind:** primitive | **Version:** 1.0.0 + +A CloudFront distribution with an S3 origin via Origin Access Control +(OAC). The distribution serves the bucket's static content from the +global edge network with HTTPS redirection by default. An optional WAF +web ACL can be associated to filter traffic before it reaches the +origin. + +## Resources + +| Resource | Type | Purpose | +|----------|------|---------| +| `oac` | `aws_cloudfront_origin_access_control` | Origin Access Control signing the S3 origin | +| `distribution` | `aws_cloudfront_distribution` | The CloudFront distribution with an S3 origin via OAC | + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `bucket_regional_domain_name` | string | yes | — | The S3 bucket regional domain name (ref to s3 origin) | +| `price_class` | string | no | `PriceClass_100` | CloudFront price class | +| `viewer_protocol_policy` | string | no | `redirect-to-https` | Viewer protocol policy | +| `default_ttl` | number | no | 3600 | Default TTL in seconds | +| `max_ttl` | number | no | 86400 | Max TTL in seconds | +| `waf_web_acl_arn` | string | no | — | WAF web ACL ARN to associate (ref to waf) | +| `region` | string | yes | — | AWS region (CloudFront is global but the provider region is used for the OAC) | + +## Outputs + +| Name | Type | Description | +|------|------|-------------| +| `distribution_arn` | arn | The CloudFront distribution ARN | +| `distribution_domain_name` | string | The CloudFront distribution domain name (e.g. d111111abcdef8.cloudfront.net) | +| `oac_id` | string | The Origin Access Control ID | + +## Usage + +```json +{ + "id": "cloudfront", + "type": "aws:cloudfront:distribution", + "module": "cloudfront@1.0.0", + "inputs": { + "bucket_regional_domain_name": "ref:s3.bucket_regional_domain_name", + "price_class": "PriceClass_100", + "viewer_protocol_policy": "redirect-to-https", + "default_ttl": 3600, + "max_ttl": 86400, + "waf_web_acl_arn": "ref:waf.web_acl_arn", + "region": "us-east-1" + } +} +``` + +The `bucket_regional_domain_name` and `waf_web_acl_arn` inputs are +typically wired as `ref:` expressions from the `s3` and `waf` primitives +inside a module composition (see `modules/l2/static-assets`). + +## Compliance extension points + +- **TLS/HTTPS** — viewer protocol policy defaults to `redirect-to-https`; + a custom ACM certificate + `viewer_certificate` block can pin TLS to a + customer domain (SOC2 CC6.1, GDPR Art.32). +- **Geo restriction** — the `restrictions.geo_restriction` block can + whitelist/blacklist countries for data-residency compliance (GDPR + Art.44, SOC2 CC6.1). +- **Logging** — CloudFront access logs to an S3 bucket for auditability + (SOC2 CC7.2, DORA audit trail). +- **Field-level encryption** — add field-level encryption for PII fields + in POST bodies (HIPAA §164.312(a)(2)(iv), GDPR Art.32). + +## Versioning + +`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps +require a new registry entry (immutable publication); old entries enter +a 12-month deprecation window. \ No newline at end of file diff --git a/modules/l1/cloudfront/interface.json b/modules/l1/cloudfront/interface.json new file mode 100644 index 0000000..9a2f06a --- /dev/null +++ b/modules/l1/cloudfront/interface.json @@ -0,0 +1,80 @@ +{ + "name": "cloudfront", + "version": "1.0.0", + "kind": "l1", + "type": "aws:cloudfront:distribution", + "description": "CloudFront distribution primitive (substrate-agnostic stack types aws:cloudfront:distribution + aws:cloudfront:originaccesscontrol; the Terraform adapter translates to aws_cloudfront_distribution + aws_cloudfront_origin_access_control).", + "inputs": { + "bucket_regional_domain_name": { + "type": "string", + "description": "The S3 bucket regional domain name (ref to s3 origin).", + "required": true + }, + "price_class": { + "type": "string", + "description": "CloudFront price class (default PriceClass_100).", + "required": false, + "default": "PriceClass_100" + }, + "viewer_protocol_policy": { + "type": "string", + "description": "Viewer protocol policy (default redirect-to-https).", + "required": false, + "default": "redirect-to-https" + }, + "default_ttl": { + "type": "number", + "description": "Default TTL in seconds (default 3600).", + "required": false, + "default": 3600 + }, + "max_ttl": { + "type": "number", + "description": "Max TTL in seconds (default 86400).", + "required": false, + "default": 86400 + }, + "waf_web_acl_arn": { + "type": "string", + "description": "WAF web ACL ARN to associate (optional, ref to waf).", + "required": false + }, + "region": { + "type": "string", + "description": "AWS region (CloudFront is global but the provider region is used for the OAC).", + "required": true + } + }, + "outputs": { + "distribution_arn": { + "type": "arn", + "description": "The CloudFront distribution ARN." + }, + "distribution_domain_name": { + "type": "string", + "description": "The CloudFront distribution domain name (e.g. d111111abcdef8.cloudfront.net)." + }, + "oac_id": { + "type": "string", + "description": "The Origin Access Control ID." + } + }, + "nfrs": {}, + "resources": [ + { + "type": "aws:cloudfront:distribution", + "description": "CloudFront distribution with S3 origin via OAC.", + "inputs": ["bucket_regional_domain_name", "price_class", "viewer_protocol_policy", "default_ttl", "max_ttl", "waf_web_acl_arn", "oac_id"], + "outputs": ["distribution_arn", "distribution_domain_name"] + }, + { + "type": "aws:cloudfront:originaccesscontrol", + "description": "Origin Access Control for the S3 origin.", + "inputs": ["name", "origin_type", "signing_behavior"], + "outputs": ["oac_id"] + } + ], + "intra_refs": [ + {"from": "aws:cloudfront:distribution.oac_id", "to": "aws:cloudfront:originaccesscontrol.oac_id"} + ] +} \ No newline at end of file diff --git a/modules/l1/s3/instance.json b/modules/l1/s3/instance.json index 80024d3..0c61ca3 100644 --- a/modules/l1/s3/instance.json +++ b/modules/l1/s3/instance.json @@ -16,7 +16,8 @@ }, "outputs": { "bucket_arn": {"type": "arn", "description": "The S3 bucket ARN."}, - "bucket_name": {"type": "string", "description": "The bucket name."} + "bucket_name": {"type": "string", "description": "The bucket name."}, + "bucket_regional_domain_name": {"type": "string", "description": "The bucket regional domain name."} } } ] diff --git a/modules/l1/s3/interface.json b/modules/l1/s3/interface.json index 497ef0e..b21734b 100644 --- a/modules/l1/s3/interface.json +++ b/modules/l1/s3/interface.json @@ -24,6 +24,10 @@ "bucket_name": { "type": "string", "description": "The bucket name (echoes the input)." + }, + "bucket_regional_domain_name": { + "type": "string", + "description": "The bucket regional domain name (e.g. acdl-spike-bucket.s3.us-east-1.amazonaws.com)." } }, "nfrs": { diff --git a/modules/l1/waf/README.md b/modules/l1/waf/README.md new file mode 100644 index 0000000..b93baae --- /dev/null +++ b/modules/l1/waf/README.md @@ -0,0 +1,67 @@ +# waf — WAFv2 Web ACL + +> **Module kind:** primitive | **Version:** 1.0.0 + +A WAFv2 Web ACL scoped to CloudFront. It applies managed rule groups by +default and can be associated with a CloudFront distribution to filter +traffic before it reaches the origin. CloudFront-scoped Web ACLs are +always created in `us-east-1`. + +## Resources + +| Resource | Type | Purpose | +|----------|------|---------| +| `webacl` | `aws_wafv2_web_acl` | WAFv2 Web ACL with managed rules | + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `name` | string | yes | — | Name of the Web ACL | +| `scope` | string | no | `cloudfront` | Scope of the Web ACL (default cloudfront for CloudFront associations) | +| `default_action` | string | no | `allow` | Default action (default allow) | +| `rules` | string | no | — | Optional custom rules as JSON (default: managed rules only) | +| `region` | string | yes | — | AWS region (CloudFront-scoped WAF is always us-east-1; the adapter ignores this for cloudfront scope) | + +## Outputs + +| Name | Type | Description | +|------|------|-------------| +| `web_acl_arn` | arn | The WAF Web ACL ARN | + +## Usage + +```json +{ + "id": "waf", + "type": "aws:wafv2:webacl", + "module": "waf@1.0.0", + "inputs": { + "name": "acdl-static-assets-waf", + "scope": "cloudfront", + "default_action": "allow", + "region": "us-east-1" + } +} +``` + +The `web_acl_arn` output is typically wired as a `ref:` expression into +the `cloudfront` primitive's `waf_web_acl_arn` input inside a module +composition (see `modules/l2/static-assets`). + +## Compliance extension points + +- **Rate limiting** — add a rate-based rule to cap requests per IP + (SOC2 CC6.1, DORA operational resilience). +- **Geo blocking** — add a geo-match statement to block/allow countries + for data-residency compliance (GDPR Art.44, SOC2 CC6.1). +- **Custom rules** — add custom rule statements for application-specific + filtering (e.g. block SQLi/IP allow-lists) (SOC2 CC6.1). +- **Logging** — enable WAF access logging to S3/CloudWatch/Kinesis for + auditability (SOC2 CC7.2, DORA audit trail). + +## Versioning + +`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps +require a new registry entry (immutable publication); old entries enter +a 12-month deprecation window. \ No newline at end of file diff --git a/modules/l1/waf/interface.json b/modules/l1/waf/interface.json new file mode 100644 index 0000000..6045fbe --- /dev/null +++ b/modules/l1/waf/interface.json @@ -0,0 +1,51 @@ +{ + "name": "waf", + "version": "1.0.0", + "kind": "l1", + "type": "aws:wafv2:webacl", + "description": "WAFv2 Web ACL primitive for CloudFront (substrate-agnostic stack type aws:wafv2:webacl; the Terraform adapter translates to aws_wafv2_web_acl). CloudFront-scoped WAF is always in us-east-1.", + "inputs": { + "name": { + "type": "string", + "description": "Name of the Web ACL.", + "required": true + }, + "scope": { + "type": "string", + "description": "Scope of the Web ACL (default cloudfront for CloudFront associations).", + "required": false, + "default": "cloudfront" + }, + "default_action": { + "type": "string", + "description": "Default action (default allow).", + "required": false, + "default": "allow" + }, + "rules": { + "type": "string", + "description": "Optional custom rules as JSON (default: managed rules only).", + "required": false + }, + "region": { + "type": "string", + "description": "AWS region (CloudFront-scoped WAF is always us-east-1; the adapter ignores this for cloudfront scope).", + "required": true + } + }, + "outputs": { + "web_acl_arn": { + "type": "arn", + "description": "The WAF Web ACL ARN." + } + }, + "nfrs": {}, + "resources": [ + { + "type": "aws:wafv2:webacl", + "description": "WAFv2 Web ACL with managed rules.", + "inputs": ["name", "scope", "default_action", "rules"], + "outputs": ["web_acl_arn"] + } + ] +} \ No newline at end of file diff --git a/modules/l2/microservice/README.md b/modules/l2/microservice/README.md index 59984dd..e007c66 100644 --- a/modules/l2/microservice/README.md +++ b/modules/l2/microservice/README.md @@ -41,7 +41,7 @@ The pattern references these primitives: Define a contract referencing this module: ```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 +uses: acdl/pipelines/deploy.yaml@v1.6 module: microservice environment: dev inputs: diff --git a/modules/l2/static-asset/README.md b/modules/l2/static-asset/README.md deleted file mode 100644 index bbe0b9a..0000000 --- a/modules/l2/static-asset/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# static-asset — S3 static asset - -> **Module kind:** module pattern | **Version:** 1.0.0 - -A pattern that references the `s3` primitive to deploy a single -S3 bucket for static asset hosting. - -## Resources - -The pattern references this primitive: - -| Primitive | Purpose | README | -|-----------|---------|--------| -| `s3` | S3 bucket | [README](../l1/s3/README.md) | - -## Inputs - -| Name | Type | Required | Description | -|------|------|----------|-------------| -| `bucket_name` | string | yes | Globally-unique S3 bucket name | -| `region` | string | yes | AWS region the bucket is created in | - -## Outputs - -| Name | Type | Description | -|------|------|-------------| -| `bucket_arn` | arn | The S3 bucket ARN | -| `bucket_name` | string | The bucket name (echoes the input) | - -## Usage - -Define a contract referencing this module: - -```yaml -uses: acdl/pipelines/deploy.yaml@v1.4 -module: static-asset -environment: dev -inputs: - bucket_name: my-static-assets - region: us-east-1 -``` - -See the [consumer guide](../../docs/consumer-guide.md) for a -step-by-step walkthrough, and the [s3 README](../l1/s3/README.md) for the -underlying primitive. - -## Compliance extension points - -The pattern can wire compliance resources when the compliance -milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands: - -- **KMS key** — shared encryption key for S3 SSE. -- **S3 access logs** — access logging to a separate audit bucket. -- **Object Lock** — 7-year immutable retention for evidence. -- **Public access block** — prevent data exfiltration. - -See the [s3 README](../l1/s3/README.md) for per-module compliance extension -points. - -## Versioning - -`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps -require a new registry entry (immutable publication); old entries enter -a 12-month deprecation window. \ No newline at end of file diff --git a/modules/l2/static-asset/composition.json b/modules/l2/static-asset/composition.json deleted file mode 100644 index 081be0b..0000000 --- a/modules/l2/static-asset/composition.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "static-asset", - "version": "1.0.0", - "kind": "l2", - "depth": 1, - "description": "A composition that references the s3 L1 primitive to deploy a single S3 bucket for static asset hosting.", - "children": [ - { - "id": "s3", - "module": "s3@1.0.0" - } - ], - "wires": [ - {"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"}, - {"from": "contract.inputs.region", "to": "s3.inputs.region"} - ], - "outputs": [ - {"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"}, - {"from": "s3.outputs.bucket_name", "to": "stack.outputs.bucket_name"} - ] -} \ No newline at end of file diff --git a/modules/l2/static-assets/README.md b/modules/l2/static-assets/README.md new file mode 100644 index 0000000..647bf15 --- /dev/null +++ b/modules/l2/static-assets/README.md @@ -0,0 +1,100 @@ +# static-assets — Production static asset stack (S3 + CloudFront + WAF) + +> **Module kind:** module pattern | **Version:** 1.0.0 + +A production-ready pattern that references the `s3`, `cloudfront`, and +`waf` primitives to deploy a static asset site with a CloudFront CDN +edge (S3 origin via Origin Access Control) and WAF protection. A simpler +S3-only variation is documented below. + +## Resources + +The pattern references these primitives: + +| Primitive | Purpose | README | +|-----------|---------|--------| +| `s3` | S3 bucket (origin) | [README](../l1/s3/README.md) | +| `cloudfront` | CloudFront distribution + OAC | [README](../l1/cloudfront/README.md) | +| `waf` | WAFv2 Web ACL (CloudFront-scoped) | [README](../l1/waf/README.md) | + +## Inputs + +| Name | Type | Required | Description | +|------|------|----------|-------------| +| `bucket_name` | string | yes | Globally-unique S3 bucket name | +| `region` | string | yes | AWS region the bucket is created in | + +## Outputs + +| Name | Type | Description | +|------|------|-------------| +| `distribution_domain_name` | string | The CloudFront distribution domain name | +| `bucket_arn` | arn | The S3 bucket ARN | +| `web_acl_arn` | arn | The WAF Web ACL ARN | + +## Usage + +Define a contract referencing this module (the production stack — +S3 + CloudFront + WAF): + +```yaml +uses: acdl/pipelines/deploy.yaml@v1.6 +module: static-assets +environment: dev +inputs: + bucket_name: my-static-assets + region: us-east-1 +``` + +The composition wires the s3 bucket regional domain name to the +CloudFront origin and the WAF web ACL ARN to the CloudFront distribution +automatically. + +### Simpler variation (S3 only) + +For a simple S3-only static asset site (no CDN, no WAF), a consumer can +declare a contract that references the `s3` primitive directly: + +```yaml +uses: acdl/pipelines/deploy.yaml@v1.6 +module: s3 +environment: dev +inputs: + bucket_name: my-static-assets + region: us-east-1 +``` + +This deploys a single S3 bucket (no CloudFront edge, no WAF). Use this +for low-traffic internal sites where a CDN edge is not required. + +See the [consumer guide](../../docs/consumer-guide.md) for a +step-by-step walkthrough, and the [s3](../l1/s3/README.md), +[cloudfront](../l1/cloudfront/README.md), and [waf](../l1/waf/README.md) +READMEs for the underlying primitives. + +## Compliance extension points + +The pattern can wire compliance resources when the compliance +milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands: + +- **KMS key** — shared encryption key for S3 SSE. +- **S3 access logs** — access logging to a separate audit bucket. +- **Object Lock** — 7-year immutable retention for evidence. +- **Public access block** — prevent data exfiltration. +- **CloudFront TLS/HTTPS** — viewer protocol policy defaults to + `redirect-to-https`; a custom ACM certificate can pin TLS to a + customer domain. +- **CloudFront geo restriction** — whitelist/blacklist countries for + data-residency compliance. +- **CloudFront logging** — access logs to an S3 bucket for auditability. +- **WAF rate limiting / geo blocking / custom rules / logging** — see + the [waf README](../l1/waf/README.md). + +See the per-primitive READMEs for the per-module compliance extension +points. + +## Versioning + +`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps +require a new registry entry (immutable publication); old entries enter +a 12-month deprecation window. \ No newline at end of file diff --git a/modules/l2/static-assets/composition.json b/modules/l2/static-assets/composition.json new file mode 100644 index 0000000..984c8a0 --- /dev/null +++ b/modules/l2/static-assets/composition.json @@ -0,0 +1,25 @@ +{ + "name": "static-assets", + "version": "1.0.0", + "kind": "l2", + "depth": 1, + "description": "A production-ready pattern that references the s3, cloudfront, and waf primitives to deploy a static asset site with a CloudFront CDN edge and WAF protection.", + "children": [ + {"id": "s3", "module": "s3@1.0.0"}, + {"id": "cloudfront", "module": "cloudfront@1.0.0"}, + {"id": "waf", "module": "waf@1.0.0"} + ], + "wires": [ + {"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"}, + {"from": "contract.inputs.region", "to": "s3.inputs.region"}, + {"from": "contract.inputs.region", "to": "cloudfront.inputs.region"}, + {"from": "contract.inputs.region", "to": "waf.inputs.region"}, + {"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"}, + {"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"} + ], + "outputs": [ + {"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"}, + {"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"}, + {"from": "waf.outputs.web_acl_arn", "to": "stack.outputs.web_acl_arn"} + ] +} \ No newline at end of file diff --git a/modules/registry.json b/modules/registry.json index b5acf74..1f23973 100644 --- a/modules/registry.json +++ b/modules/registry.json @@ -48,9 +48,23 @@ "deprecated": false } }, - "static-asset": { + "cloudfront": { "1.0.0": { - "interface": "modules/l2/static-asset/composition.json", + "interface": "modules/l1/cloudfront/interface.json", + "published_at": "2026-07-22T19:00:00Z", + "deprecated": false + } + }, + "waf": { + "1.0.0": { + "interface": "modules/l1/waf/interface.json", + "published_at": "2026-07-22T19:00:00Z", + "deprecated": false + } + }, + "static-assets": { + "1.0.0": { + "interface": "modules/l2/static-assets/composition.json", "published_at": "2026-07-22T15:00:00Z", "deprecated": false } diff --git a/pipelines/deploy.yaml b/pipelines/deploy.yaml index df8d4e9..6587011 100644 --- a/pipelines/deploy.yaml +++ b/pipelines/deploy.yaml @@ -22,17 +22,17 @@ python_version: "3.12" stages: - name: validate-contract description: Validate the consumer contract against the contract schema - command: python3 -c "import jsonschema, yaml; jsonschema.validate(yaml.safe_load(open('contracts/static-asset.yaml')), json.load(open('schemas/contract.schema.json')))" + command: python3 -c "import jsonschema, yaml; jsonschema.validate(yaml.safe_load(open('contracts/static-assets.yaml')), json.load(open('schemas/contract.schema.json')))" required: true - name: resolve-stack description: Resolve the contract to a Target Stack instance via the contract resolver - command: python3 core/contract_resolver.py contracts/static-asset.yaml /tmp/acdl-stack.json + command: python3 core/contract_resolver.py contracts/static-assets.yaml /tmp/acdl-stack.json required: true - name: terraform-plan description: Compile the stack to Terraform and run terraform plan - command: bash scripts/run_platform.sh --plan-only contracts/static-asset.yaml + command: bash scripts/run_platform.sh --plan-only contracts/static-assets.yaml required: true - name: checkov diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 19068ac..7a69328 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -65,7 +65,7 @@ fi # Default contract for --check-only (CI uses this) if [ -z "$CONTRACT" ]; then if [ "$CHECK_ONLY" = "1" ]; then - CONTRACT="contracts/static-asset.yaml" + CONTRACT="contracts/static-assets.yaml" else echo "FAIL: contract file required (usage: run_platform.sh )" >&2 exit 1 @@ -135,7 +135,7 @@ if [ "$CHECK_ONLY" = "1" ]; then python3 -c " import json, os d = json.load(open('$WORK/stack.json')) -assert d['stack']['name'] == 'static-asset', f\"expected static-asset, got {d['stack']['name']}\" +assert d['stack']['name'] == 'static-assets', f\"expected static-assets, got {d['stack']['name']}\" assert len(d['resources']) >= 1 tf_dir = 'terraform/spike' for f in ('main.tf', 'terraform.tf', 'providers.tf'): diff --git a/terraform/spike/main.tf b/terraform/spike/main.tf index d750400..a2717b4 100644 --- a/terraform/spike/main.tf +++ b/terraform/spike/main.tf @@ -12,3 +12,88 @@ output "bucket_arn" { output "bucket_name" { value = aws_s3_bucket.s3.id } + +output "bucket_regional_domain_name" { + value = aws_s3_bucket.s3.bucket_regional_domain_name +} + +resource "aws_cloudfront_distribution" "cloudfront-distribution" { + origin { + domain_name = aws_s3_bucket.s3.bucket_regional_domain_name + origin_access_control = aws_cloudfront_origin_access_control.cloudfront-originaccesscontrol.id + s3_origin_config {} + } + enabled = true + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + target_origin_id = "cloudfront-distribution" + min_ttl = 0 + default_ttl = 3600 + max_ttl = 86400 + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + } + price_class = "PriceClass_100" + restrictions { + geo_restriction { + restriction_type = "none" + } + } + viewer_certificate { + cloudfront_default_certificate = true + } + web_acl_id = aws_wafv2_web_acl.waf.arn +} + +output "distribution_arn" { + value = aws_cloudfront_distribution.cloudfront-distribution.arn +} + +output "distribution_domain_name" { + value = aws_cloudfront_distribution.cloudfront-distribution.domain_name +} + +resource "aws_cloudfront_origin_access_control" "cloudfront-originaccesscontrol" { + name = "acdl-oac" + origin_access_control_origin_type = "s3" + origin_access_control_signing_behavior = "always" +} + +output "oac_id" { + value = aws_cloudfront_origin_access_control.cloudfront-originaccesscontrol.id +} + +resource "aws_wafv2_web_acl" "waf" { + name = "acdl-waf" + scope = "cloudfront" + default_action { + allow {} + } + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "acdl-waf-metrics" + sampled_requests_enabled = true + } + rules { + name = "aws-managed-rules" + priority = 0 + override_action { + none {} + } + statement { + managed_rule_group_statement { + name = "AWSManagedRulesCommonRuleSet" + vendor_name = "AWS" + } + } + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "aws-managed-rules-metrics" + sampled_requests_enabled = true + } + } +} + +output "web_acl_arn" { + value = aws_wafv2_web_acl.waf.arn +} diff --git a/terraform/spike/terraform.tf b/terraform/spike/terraform.tf index b446c4c..552d586 100644 --- a/terraform/spike/terraform.tf +++ b/terraform/spike/terraform.tf @@ -8,7 +8,7 @@ terraform { } backend "s3" { bucket = "acdl-tfstate-581513795199-us-east-1" - key = "spike/static-asset/terraform.tfstate" + key = "spike/static-assets/terraform.tfstate" region = "us-east-1" } } diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 82a11f0..13a4931 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -31,14 +31,14 @@ class TestInstance: class TestRegistry: - EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr"} - EXPECTED_L2_KEYS = {"static-asset", "microservice"} + EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf"} + EXPECTED_L2_KEYS = {"static-assets", "microservice"} - def test_registry_has_9_entries(self, registry): - assert len(registry) == 9 + def test_registry_has_11_entries(self, registry): + assert len(registry) == 11 assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS) - def test_registry_has_7_l1_entries(self, registry): + def test_registry_has_9_l1_entries(self, registry): l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")} assert l1 == self.EXPECTED_L1_KEYS @@ -78,6 +78,13 @@ class TestTypeMap: assert TYPE_MAP["aws:iam:role"] == "aws_iam_role" assert TYPE_MAP["aws:ecr:repository"] == "aws_ecr_repository" + def test_cloudfront_types_in_type_map(self): + assert TYPE_MAP["aws:cloudfront:distribution"] == "aws_cloudfront_distribution" + assert TYPE_MAP["aws:cloudfront:originaccesscontrol"] == "aws_cloudfront_origin_access_control" + + def test_waf_type_in_type_map(self): + assert TYPE_MAP["aws:wafv2:webacl"] == "aws_wafv2_web_acl" + class TestTfValue: def test_string_quoted(self): @@ -177,4 +184,57 @@ class TestAdapt: out_dir = str(tmp_path / "tf_out") adapt(stack_instance, out_dir) terraform_tf = open(os.path.join(out_dir, "terraform.tf")).read() - assert "spike/s3/terraform.tfstate" in terraform_tf \ No newline at end of file + assert "spike/s3/terraform.tfstate" in terraform_tf + + +class TestS3Output: + def test_s3_instance_has_bucket_regional_domain_name_output(self, stack_instance, tmp_path): + out_dir = str(tmp_path / "tf_out") + adapt(stack_instance, out_dir) + main_tf = open(os.path.join(out_dir, "main.tf")).read() + assert 'output "bucket_regional_domain_name"' in main_tf + assert "aws_s3_bucket.s3.bucket_regional_domain_name" in main_tf + + +class TestStaticAssetsStack: + @pytest.fixture + def static_assets_stack(self): + from core.contract_resolver import resolve + return resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT)) + + def test_static_assets_resolves_to_4_resources(self, static_assets_stack): + types = [r["type"] for r in static_assets_stack["resources"]] + assert "aws:s3:bucket" in types + assert "aws:cloudfront:distribution" in types + assert "aws:cloudfront:originaccesscontrol" in types + assert "aws:wafv2:webacl" in types + + def test_static_assets_adapter_emits_all_resources(self, static_assets_stack, tmp_path): + out_dir = str(tmp_path / "tf_out") + adapt(static_assets_stack, out_dir) + main_tf = open(os.path.join(out_dir, "main.tf")).read() + assert 'resource "aws_s3_bucket" "s3"' in main_tf + assert 'resource "aws_cloudfront_distribution" "cloudfront-distribution"' in main_tf + assert 'resource "aws_cloudfront_origin_access_control" "cloudfront-originaccesscontrol"' in main_tf + assert 'resource "aws_wafv2_web_acl" "waf"' in main_tf + + def test_static_assets_adapter_wires_s3_origin_to_cloudfront(self, static_assets_stack, tmp_path): + out_dir = str(tmp_path / "tf_out") + adapt(static_assets_stack, out_dir) + main_tf = open(os.path.join(out_dir, "main.tf")).read() + assert "aws_s3_bucket.s3.bucket_regional_domain_name" in main_tf + assert "aws_cloudfront_origin_access_control.cloudfront-originaccesscontrol.id" in main_tf + + def test_static_assets_adapter_wires_waf_to_cloudfront(self, static_assets_stack, tmp_path): + out_dir = str(tmp_path / "tf_out") + adapt(static_assets_stack, out_dir) + main_tf = open(os.path.join(out_dir, "main.tf")).read() + assert "aws_wafv2_web_acl.waf.arn" in main_tf + assert "web_acl_id = aws_wafv2_web_acl.waf.arn" in main_tf + + def test_static_assets_adapter_emits_distribution_outputs(self, static_assets_stack, tmp_path): + out_dir = str(tmp_path / "tf_out") + adapt(static_assets_stack, out_dir) + main_tf = open(os.path.join(out_dir, "main.tf")).read() + assert 'output "distribution_domain_name"' in main_tf + assert 'output "web_acl_arn"' in main_tf \ No newline at end of file diff --git a/tests/test_contract_resolver.py b/tests/test_contract_resolver.py index f2d6be7..896a147 100644 --- a/tests/test_contract_resolver.py +++ b/tests/test_contract_resolver.py @@ -26,15 +26,24 @@ class TestContractSchema: class TestResolveStaticAsset: def test_resolve_static_asset_contract(self, tmp_path): from core.contract_resolver import resolve - stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT)) - assert stack["stack"]["name"] == "static-asset" + stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT)) + assert stack["stack"]["name"] == "static-assets" assert stack["stack"]["kind"] == "l2" assert stack["stack"]["depth"] == 1 assert len(stack["resources"]) >= 1 + def test_resolve_static_asset_has_s3_cloudfront_waf_resources(self): + from core.contract_resolver import resolve + stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT)) + types = [r["type"] for r in stack["resources"]] + assert "aws:s3:bucket" in types + assert "aws:cloudfront:distribution" in types + assert "aws:cloudfront:originaccesscontrol" in types + assert "aws:wafv2:webacl" in types + def test_resolve_static_asset_has_s3_resource(self): from core.contract_resolver import resolve - stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT)) + stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT)) s3_res = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"] assert len(s3_res) == 1 assert s3_res[0]["inputs"]["bucket_name"] == "acdl-spike-bucket" @@ -42,7 +51,7 @@ class TestResolveStaticAsset: def test_resolve_static_asset_validates_against_stack_schema(self): from core.contract_resolver import resolve - stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT)) + stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT)) schema = json.load(open(ROOT / "schemas/stack.schema.json")) jsonschema.validate(stack, schema) diff --git a/tests/test_environment_check.py b/tests/test_environment_check.py index 204078d..9cb609c 100644 --- a/tests/test_environment_check.py +++ b/tests/test_environment_check.py @@ -32,15 +32,15 @@ class TestEnvironmentCheck: assert "IAM role" in msg def test_contract_with_dev_environment_passes(self): - ok, msg = check(contract_path=str(ROOT / "contracts/static-asset.yaml"), root=ROOT) + ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yaml"), root=ROOT) assert ok is True assert "dev" in msg def test_contract_with_missing_environment_fails(self, tmp_path): contract = tmp_path / "contract.yaml" contract.write_text( - "uses: acdl/pipelines/deploy.yaml@v1.4\n" - "module: static-asset\n" + "uses: acdl/pipelines/deploy.yaml@v1.6\n" + "module: static-assets\n" "environment: no-such-env\n" "inputs:\n bucket_name: x\n region: us-east-1\n" ) @@ -55,8 +55,8 @@ class TestEnvironmentCheck: def test_contract_without_environment_field_returns_false(self, tmp_path): contract = tmp_path / "contract.yaml" contract.write_text( - "uses: acdl/pipelines/deploy.yaml@v1.4\n" - "module: static-asset\n" + "uses: acdl/pipelines/deploy.yaml@v1.6\n" + "module: static-assets\n" "inputs:\n bucket_name: x\n region: us-east-1\n" ) ok, msg = check(contract_path=str(contract), root=ROOT) diff --git a/tests/test_pipeline_contract.py b/tests/test_pipeline_contract.py index e673e4a..3bf8601 100644 --- a/tests/test_pipeline_contract.py +++ b/tests/test_pipeline_contract.py @@ -354,8 +354,8 @@ class TestDeployWorkflowConformance: class TestSampleContractVersioning: def test_sample_contract_uses_versioned_tag(self): - contract = _load_yaml("contracts/static-asset.yaml") + contract = _load_yaml("contracts/static-assets.yaml") uses = contract["uses"] assert "@v" in uses, "sample contract must use a versioned @vX.Y tag" assert "@main" not in uses, "sample contract must not use @main" - assert uses == "acdl/pipelines/deploy.yaml@v1.4" \ No newline at end of file + assert uses == "acdl/pipelines/deploy.yaml@v1.6" \ No newline at end of file