Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2861319447 | |||
| f9a93d56cc | |||
| ca99241843 | |||
| c99da9a58c | |||
| da60f0e82f | |||
| 3562f6f771 | |||
| cb02c69e0c | |||
| 134f85d2df | |||
| 491ba78768 | |||
| 8145eee8fc | |||
| de91a4bb76 | |||
| 1e4133e11a | |||
| 843cd17b97 | |||
| 0eb578c606 | |||
| 045c7279aa | |||
| 7f1eff622d | |||
| 60f2b669ea | |||
| bab2cf363b | |||
| e597c0b089 | |||
| 2e2064559a | |||
| f2230edae0 | |||
| 0bee8f9bc2 | |||
| f3b7815120 | |||
| 94065a4fbc | |||
| 4bd07a4fae | |||
| a9d8b31595 | |||
| 49462d5e38 | |||
| a4b17d0f26 | |||
| 90be5839ab | |||
| 4fe794c7a4 | |||
| 07c0349131 | |||
| 1fd37a2843 | |||
| dca35c78ec | |||
| 2732abb23f | |||
| b026d5f041 | |||
| fee59944fd | |||
| 05372abdfc |
@@ -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
|
||||
@@ -342,8 +342,8 @@ expands the adapter `TYPE_MAP` to cover the six new ECS-shaped IR resource
|
||||
types. The L1 interface shape (IR-typed inputs/outputs/NFRs, registered in
|
||||
`modules-ir/registry.json`) is unchanged — only the set of registered L1s
|
||||
grows. The IR commitments (REQ-28) continue to hold: `modules-ir/`,
|
||||
`schemas/`, `contracts/`, `acdl_platform/confidence_signal.py`,
|
||||
`acdl_platform/contract_resolver.py`, `acdl_platform/outbox_writer.py`
|
||||
`schemas/`, `contracts/`, `core/confidence_signal.py`,
|
||||
`core/contract_resolver.py`, `core/outbox_writer.py`
|
||||
remain substrate-agnostic.
|
||||
|
||||
### `terraform apply` (dev only)
|
||||
@@ -375,4 +375,72 @@ stream as a `terraform.apply` event.
|
||||
4. Phase 14 — `l2-microservice` + contract schema extension.
|
||||
5. Phase 15 — consumer repo + `terraform apply` (dev) → live ECS service.
|
||||
6. Phase 16 — capstone e2e: consumer commit → live HTTP 200 → evidence → timeline.
|
||||
7. COMPLETE gate — review → ship `v1.3.0` → audit.
|
||||
7. COMPLETE gate — review → ship `v1.3.0` → audit.
|
||||
|
||||
## v1.8 Architecture Addendum
|
||||
|
||||
> Milestone v1.8 (complete, tag `v1.8.0`). Adds encryption-by-default,
|
||||
> deletion-protection-by-default, uptime monitoring, decommission alias,
|
||||
> engineering standards, and path documentation.
|
||||
|
||||
### New Primitives
|
||||
|
||||
- **`kms-key`** (`aws:kms:key`) — Per-stack customer-managed KMS key with
|
||||
`enable_key_rotation = true`. One key per L2 deployment (no shared keys).
|
||||
Wired into both L2 compositions as a child, with its `kms_key_arn` output
|
||||
connected to all children's `kms_key_arn` input. Adapter emits
|
||||
`aws_kms_key` + `enable_key_rotation`.
|
||||
- **`uptime`** (`aws:ecs:uptime-service`) — Uptime-kuma on ECS Fargate with
|
||||
a feature flag (`feature_flag_enabled`), monitored endpoints (HTTP/DNS/TCP),
|
||||
alert channels (Teams/email/SMS/GitHub issues). Deployed by default after
|
||||
any L2 module with a separate terraform state. When the feature flag is
|
||||
false, the adapter emits no resources.
|
||||
|
||||
### Encryption by Default
|
||||
|
||||
All 12 L1 primitives have `encryption_enabled` NFR (default true). Primitives
|
||||
with at-rest data (s3, rds, ecr, ecs-service, ecs-cluster) have an optional
|
||||
`kms_key_arn` input. The adapter emits encryption blocks (SSE-KMS for S3,
|
||||
storage_encrypted for RDS, encryption_configuration for ECR) referencing the
|
||||
per-stack CMK when provided. Managed KMS fallback with stderr warning for
|
||||
standalone L1 deployments.
|
||||
|
||||
### Deletion Protection by Default
|
||||
|
||||
All 12 L1 primitives have `deletion_protection` NFR (default true). The
|
||||
adapter emits `lifecycle { prevent_destroy = true }` when true. L2 modules
|
||||
expose a `features.deletion_protection` flag (default true) propagated to
|
||||
all children via the resolver. Setting `inputs.deletion_protection: false`
|
||||
in the contract disables it for the whole stack.
|
||||
|
||||
### Decommission Alias
|
||||
|
||||
A `mode: decommission` on the deploy pipeline implements a 2-step destroy:
|
||||
1. Disable deletion protection (resolve with `deletion_protection: false`,
|
||||
terraform plan/apply, HITL SRE gate via GitHub environment).
|
||||
2. Zero counts + destroy (`decommission_transform` zeroes all scalable counts,
|
||||
terraform plan/apply, second HITL SRE gate).
|
||||
|
||||
CMDB validation via DynamoDB `acdl-change-requests` table. The Lambda
|
||||
`validate_change_request` action queries the table and asserts
|
||||
`status == "approved"` + `consumerRepo` match.
|
||||
|
||||
### Adapter Expansion
|
||||
|
||||
TYPE_MAP grew from 16 to 19 entries (+ `aws:kms:key`, `aws:kms:alias`,
|
||||
`aws:ecs:uptime-service`). Specialized emission branches added for KMS key
|
||||
rotation, S3 SSE-KMS configuration, uptime ECS Fargate task, and
|
||||
`prevent_destroy` lifecycle on all resources.
|
||||
|
||||
### Pipeline Stages
|
||||
|
||||
The deploy pipeline grew from 8 to 9 stages (+ `deploy-uptime` after
|
||||
`publish-outputs`). The `deploy-uptime` stage constructs a synthetic uptime
|
||||
contract from the L2 stack outputs, resolves + adapts it to a separate
|
||||
terraform state directory, and publishes the uptime URL via PR comment.
|
||||
|
||||
### Forge-Agnostic API URLs
|
||||
|
||||
The platform Lambda (`contract_ingestor.py`) reads `GITHUB_API_BASE` env
|
||||
for forge-agnostic API URLs. GitHub uses `/search/issues`; Gitea uses
|
||||
`/repos/{owner}/{repo}/issues`. Detection via `/api/v1` in the base URL.
|
||||
+55
-35
@@ -1,10 +1,10 @@
|
||||
---
|
||||
project: acdl
|
||||
milestone: v1.1
|
||||
generated_at: 2026-07-21
|
||||
milestone: v1.8
|
||||
generated_at: 2026-07-22
|
||||
generator: lead-developer
|
||||
verification_toolchain:
|
||||
typecheck: "terraform validate && python3 -m py_compile acdl_platform/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
||||
typecheck: "terraform validate && python3 -m py_compile core/**/*.py && python3 -m jsonschema schemas/*.schema.json"
|
||||
test: "scripts/verify_phaseNN.sh"
|
||||
build: "terraform init"
|
||||
note: |
|
||||
@@ -16,7 +16,7 @@ verification_toolchain:
|
||||
ci-* agents read PERSONAS.md before running verification commands.
|
||||
---
|
||||
|
||||
# ACDL — Persona Roster (project-level, v1.1)
|
||||
# ACDL — Persona Roster (project-level, v1.8)
|
||||
|
||||
## Active personas
|
||||
|
||||
@@ -27,34 +27,43 @@ verification_toolchain:
|
||||
- **Frameworks:** (none)
|
||||
- **Constraints:** pragmatic, battle-tested defaults, no-cross-territory-edits, vision-is-source-of-truth-for-why
|
||||
- **Territory:** `.ciagent/**`, `scripts/verify_phase*.sh`, `README.md`, `docs/**` (meta only — not architecture authoring), `.gitignore`
|
||||
- **Reason:** Owns CIAgent metadata, cross-phase verification scripts, and the v1.1 phase orchestration. Resolves the 11 open decisions (D-038) and arbitrates persona conflicts.
|
||||
- **Reason:** Owns CIAgent metadata, cross-phase verification scripts, and the v1.7 phase orchestration. Resolves the 12-scope-axis decomposition (D-048→D-060) and arbitrates persona conflicts.
|
||||
|
||||
### backend-engineer
|
||||
- **Domain:** backend
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** python, json-schema, gitea-actions, act_runner, bash, yaml
|
||||
- **Frameworks:** python, json-schema, gitea-actions, act_runner, bash, yaml, github-actions
|
||||
- **Constraints:** contract-schema-first, fail-fast-with-reason-codes, no-long-lived-credentials, severity-to-penalty-mapping-immutable
|
||||
- **Territory:** `acdl_platform/confidence_signal.py`, `acdl_platform/contract_resolver.py`, `acdl_platform/outbox_writer.py`, `schemas/**` (contract + IR + PolicyCheckResult), `contracts/**` (sample contracts), `.gitea/workflows/**` (pipeline)
|
||||
- **Reason:** Owns the contract schema, contract→IR resolution, the confidence signal (6 inputs + severity mapping), the DynamoDB outbox writer, and the central pipeline workflow.
|
||||
- **Territory:** `core/confidence_signal.py`, `core/contract_resolver.py`, `core/outbox_writer.py`, `core/output_publisher.py`, `core/environment_check.py`, `schemas/**` (contract + IR + PolicyCheckResult + tagging-standard + pipeline), `contracts/**` (sample contracts), `.gitea/workflows/**` + `.github/workflows/**` (pipeline + deploy + platform-test + primitives-plan + patterns-plan + release), `pipelines/**`, `scripts/run_ci.sh`, `scripts/run_platform.sh`, `scripts/post_stage_comment.sh`, `scripts/run_primitive_plan.sh`, `scripts/run_pattern_plan.sh`
|
||||
- **Reason:** Owns the contract schema, contract→IR resolution, the confidence signal (6 inputs + severity mapping), the DynamoDB outbox writer, the output publisher (SSM + GitHub comment), the central pipeline workflows (CI + deploy + platform-test + primitives-plan + patterns-plan + release), and the deploy-pipeline DX (stage comments, error-report step).
|
||||
|
||||
### platform-engineer (custom)
|
||||
- **Domain:** infra
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** terraform, aws-iam, aws-s3, aws-dynamodb, oidc, json-schema
|
||||
- **Constraints:** ir-is-substrate-agnostic, adapter-is-only-substrate-specific-code, state-in-s3+dynamodb-single-region, oidc-only-no-long-lived-keys (waiver D-034 for bootstrap), terraform-plan-only-in-spike
|
||||
- **Territory:** `adapters/terraform/**`, `modules-ir/**`, `terraform/**` (state backend, provider config), `modules-ir/registry.json`
|
||||
- **Reason:** Owns the Target Stack IR, the L1/L2 IR-typed modules, the Terraform adapter, the AWS OIDC bootstrap, and the state backend. The IR is substrate-agnostic; the adapter is the only substrate-specific code (the binding constraint per §12).
|
||||
- **Frameworks:** terraform, aws-iam, aws-s3, aws-dynamodb, aws-lambda, aws-cloudfront, aws-waf, aws-ssm, aws-secretsmanager, oidc, json-schema
|
||||
- **Constraints:** ir-is-substrate-agnostic, adapter-is-only-substrate-specific-code, state-in-s3+dynamodb-single-region, oidc-only-no-long-lived-keys (waiver D-034 for bootstrap), terraform-plan-only-in-spike, cross-account-iam-scoped-via-abac
|
||||
- **Territory:** `adapters/terraform/**`, `modules/**` (l1 + l2 + registry.json + examples), `terraform/**` (state backend, provider config, platform infra), `modules/registry.json`
|
||||
- **Reason:** Owns the Target Stack IR, the L1/L2 IR-typed modules (incl. new cloudfront + waf + rds primitives), the Terraform adapter (TYPE_MAP expansion for cloudfront/waf/rds), the AWS OIDC bootstrap, the state backend, and the platform Terraform (Lambda + DynamoDB + KMS + Secrets Manager + Function URL). The IR is substrate-agnostic; the adapter is the only substrate-specific code (the binding constraint per §12).
|
||||
|
||||
### security-engineer (custom)
|
||||
- **Domain:** security
|
||||
- **Active:** true
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** aws-iam, oidc, checkov, json-schema
|
||||
- **Constraints:** least-privilege, separation-of-duties-identity-distinctness, no-secrets-in-skill-markdown, audit-chain-extends-not-tears-up, critical-finding-hard-overrides-confidence
|
||||
- **Territory:** `acdl_platform/hitl_matrix_design.md`, `acdl_platform/audit_ledger_design.md`, `adapters/terraform/policy/**` (Checkov adapter → PolicyCheckResult), `acdl_platform/separation_of_duties.py`
|
||||
- **Reason:** Owns the HITL matrix design, separation-of-duties (DynamoDB identity-distinctness), the audit ledger design (S3 Object Lock + JWS + chain), and the Checkov→PolicyCheckResult adapter. Enforces the "Safety is Computed, Not Assumed" + "Audit truth lives outside the repository" vision tenets.
|
||||
- **Frameworks:** aws-iam, oidc, checkov, kyverno, wiz, json-schema
|
||||
- **Constraints:** least-privilege, separation-of-duties-identity-distinctness, no-secrets-in-skill-markdown, audit-chain-extends-not-tears-up, critical-finding-hard-overrides-confidence, required-tags-enforced
|
||||
- **Territory:** `core/hitl_matrix_design.md`, `core/audit_ledger_design.md`, `adapters/terraform/policy/**` (Checkov adapter + custom rules), `adapters/wiz/**` (Wiz adapter), `adapters/kyverno/**` (Kyverno adapter + sample policies), `core/separation_of_duties.py`, `schemas/tagging-standard.json`, `schemas/policy_check_result.schema.json` (engine enum)
|
||||
- **Reason:** Owns the HITL matrix design, separation-of-duties, the audit ledger design, the Checkov→PolicyCheckResult adapter + the custom tagging rule (D-054, D-043 closure), the Wiz adapter (D-052), the Kyverno adapter (D-053), and the tagging standard. Enforces the "Safety is Computed, Not Assumed" + "Audit truth lives outside the repository" vision tenets.
|
||||
|
||||
### lambda-engineer (custom, v1.8)
|
||||
- **Domain:** serverless
|
||||
- **Active:** true
|
||||
- **Phase-specific:** true (reactivated for v1.8; removed after milestone COMPLETE)
|
||||
- **Frameworks:** python, aws-lambda, boto3, dynamodb, aws-secretsmanager, github-api, gitea-api
|
||||
- **Constraints:** lambda-is-stateless, dynamodb-is-the-state-store, secrets-from-secrets-manager-never-logged, idempotent-actions, cross-account-iam-via-abac, forge-agnostic-api-urls
|
||||
- **Territory:** `core/lambda/**` (contract_ingestor.py + handler), `terraform/platform/main.tf` (Lambda + Function URL + DynamoDB + KMS + Secrets Manager + IAM + acdl-change-requests table), `terraform/platform/consumer_invoke_policy.json`, `terraform/platform/variables.tf`
|
||||
- **Reason:** Reactivated for v1.8 Phase 29 (Terraform-rendered invoke policy), Phase 30 (forge-agnostic API URLs in contract_ingestor.py), Phase 34 (validate_change_request Lambda action + acdl-change-requests DynamoDB table). The Lambda is stateless; all state is in DynamoDB. Forge-agnostic API URLs (GitHub + Gitea) via GITHUB_API_BASE env var. Removed from the roster after milestone COMPLETE (the code persists, but the persona is no longer active).
|
||||
|
||||
### frontend-engineer
|
||||
- **Domain:** frontend
|
||||
@@ -63,21 +72,21 @@ verification_toolchain:
|
||||
- **Frameworks:** vanilla-js, dom-api, fetch-api
|
||||
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url, relative-url-for-audit-json
|
||||
- **Territory:** `evidence-ui/**` (the timeline UI; pushed to `acdl-evidence`)
|
||||
- **Reason:** Owns the evidence timeline UI (`index.html`). Carried over from v1.0; the UI continues to render the audit stream. The v1.1 spike writes events to the DynamoDB outbox; the UI continues to read `audit.json` published to `acdl-evidence`.
|
||||
- **Reason:** Owns the evidence timeline UI (`index.html`). Carried over from v1.0; the UI continues to render the audit stream. The v1.7 spike writes events to the DynamoDB outbox; the UI continues to read `audit.json` published to `acdl-evidence`.
|
||||
|
||||
## Deactivated personas
|
||||
|
||||
### infra-stub-engineer (custom, v1.0 only)
|
||||
- **Domain:** backend
|
||||
- **Active:** false
|
||||
- **Reason:** Owned L1 stub modules (`modules/l1/**`) in the v1.0 demo. The demo is archived to `demo/` in Phase 06; real L1 modules (`modules-ir/l1/**`) are owned by platform-engineer (substrate-agnostic IR + Terraform adapter). The stub engineer is no longer needed.
|
||||
- **Reason:** Owned L1 stub modules (`modules/l1/**`) in the v1.0 demo. The demo is archived to `demo/` in Phase 06; real L1 modules (`modules-ir/l1/**`, now `modules/l1/**`) are owned by platform-engineer (substrate-agnostic IR + Terraform adapter). The stub engineer is no longer needed.
|
||||
- **Phase-specific:** false (was v1.0)
|
||||
- **Territory (would have been):** `demo/modules/l1/**`
|
||||
|
||||
### data-engineer
|
||||
- **Domain:** data
|
||||
- **Active:** false
|
||||
- **Reason:** No ORM/persistence framework. The v1.1 outbox is DynamoDB but accessed via boto3 calls inside `acdl_platform/outbox_writer.py` (owned by backend-engineer); the audit ledger is S3 Object Lock + JWS (owned by security-engineer). No schema-migration layer, no ORM, no data-engineer territory.
|
||||
- **Reason:** No ORM/persistence framework. The v1.7 contract-ingestion table is DynamoDB but accessed via boto3 inside `core/lambda/contract_ingestor.py` (owned by lambda-engineer); the outbox is DynamoDB accessed via `core/outbox_writer.py` (owned by backend-engineer); the audit ledger is S3 Object Lock + JWS (owned by security-engineer). No schema-migration layer, no ORM, no data-engineer territory.
|
||||
- **Phase-specific:** false
|
||||
- **Frameworks:** (would have been: drizzle, prisma)
|
||||
- **Constraints:** (would have been: schema-first, type-safe-orm)
|
||||
@@ -87,32 +96,43 @@ verification_toolchain:
|
||||
|
||||
| Phase | Personas active | Notes |
|
||||
|-------|------------------|-------|
|
||||
| 06 archive-demo-and-reorient | lead-developer, frontend-engineer (demo UI move only) | backend/platform/security idle |
|
||||
| 07 architecture-v1-finalization | lead-developer, backend-engineer (schemas), security-engineer (HITL/ledger/SoD), platform-engineer (IR) | frontend idle |
|
||||
| 08 aws-oidc-bootstrap | platform-engineer (lead), security-engineer (trust policy review) | backend/frontend idle |
|
||||
| 09 v1-spike-ir-and-l1-and-adapter | platform-engineer (lead), backend-engineer (IR schema co-author) | security/frontend idle |
|
||||
| 10 v1-spike-l2-and-contract-e2e | platform-engineer (L2 + adapter), backend-engineer (contract→IR + confidence + outbox), security-engineer (Checkov→PolicyCheckResult), frontend-engineer (evidence event surfaces in timeline) | Full roster |
|
||||
| 28 adapter-waf-and-resolver-outputs | platform-engineer (lead: WAF HCL fix + adapter output blocks), backend-engineer (resolver outputs processing) | security/lambda/frontend idle |
|
||||
| 29 ssm-kms-and-invoke-policy | backend-engineer (lead: SSM fail-loud), lambda-engineer (Terraform-rendered invoke policy), security-engineer (CMK enforcement review) | platform/frontend idle |
|
||||
| 30 run-platform-isolation-and-api-portability | backend-engineer (lead: run_platform.sh temp dir + deploy.yml static-key), lambda-engineer (forge-agnostic API URLs) | platform/security/frontend idle |
|
||||
| 31 encryption-by-default-and-per-stack-cmk | platform-engineer (lead: kms-key primitive + adapter expansion + L2 wiring), security-engineer (encryption NFR enforcement review) | backend/lambda/frontend idle |
|
||||
| 32 deletion-protection-by-default-and-l2-feature-flag | platform-engineer (lead: prevent_destroy emission + L2 feature flag), backend-engineer (contract schema update) | security/lambda/frontend idle |
|
||||
| 33 uptime-kuma-primitive | platform-engineer (lead: uptime primitive + adapter + separate state), backend-engineer (deploy-uptime pipeline stage + run_platform.sh + PR comment) | security/lambda/frontend idle |
|
||||
| 34 decommission-alias-and-cmdb-validation | backend-engineer (lead: decommission pipeline mode + run_platform.sh + consumer docs), lambda-engineer (validate_change_request + acdl-change-requests table), security-engineer (HITL SRE gates review) | platform/frontend idle |
|
||||
| 35 module-engineering-standards | lead-developer (lead: STANDARDS.md + catalog fix + template), platform-engineer (standards content review), backend-engineer (automated standards test) | security/lambda/frontend idle |
|
||||
| 36 schemas-adapters-pipelines-readmes | lead-developer (lead: 3 READMEs), backend-engineer (pipelines + schemas README content), platform-engineer (adapters README content) | security/lambda/frontend idle |
|
||||
| 37 verify | lead-developer (lead: 4-layer verification), all personas (review their territory) | — |
|
||||
| 38 review-audit-complete | lead-developer (lead: review + audit + milestone completion), all personas (review participation) | — |
|
||||
|
||||
## Domain priority (used by TaskDecomposer)
|
||||
|
||||
`coordination → security → platform → backend → frontend`
|
||||
`coordination → security → platform → backend → lambda → frontend`
|
||||
|
||||
Rationale: in v1.1, the security/architecture commitments (IR, confidence,
|
||||
HITL, ledger, SoD) are the binding constraints; the platform layer
|
||||
materializes them; backend wires the pipeline; frontend surfaces the
|
||||
evidence. The spike's correctness depends on the security + platform layers
|
||||
being right before backend wiring.
|
||||
Rationale: in v1.8, the security commitments (encryption by default,
|
||||
KMS rotation, deletion protection, CMDB validation, HITL SRE gates)
|
||||
and the platform commitments (kms-key primitive, uptime primitive,
|
||||
adapter expansion, prevent_destroy emission) are the binding
|
||||
constraints; backend wires the pipeline + decommission mode + API
|
||||
portability; lambda owns the CMDB validation + forge-agnostic APIs;
|
||||
frontend is unchanged from v1.0 (evidence timeline).
|
||||
|
||||
## Conflict resolutions (lead-developer arbitration)
|
||||
|
||||
- `backend-engineer` vs `platform-engineer` over `schemas/ir.schema.json`: platform-engineer owns the IR (it is substrate-agnostic but infra-shaped); backend-engineer owns the contract schema and the contract→IR resolution (contract is the consumer surface). Co-authoring is expected; conflict goes to lead-developer.
|
||||
- `backend-engineer` vs `security-engineer` over `acdl_platform/confidence_signal.py`: security-engineer owns the severity→penalty mapping + critical-override semantics; backend-engineer owns the 6-input weighted sum + per-env thresholds. The confidence signal is co-owned; conflicts go to lead-developer.
|
||||
- `platform-engineer` vs `security-engineer` over `adapters/terraform/policy/**`: security-engineer owns the Checkov→PolicyCheckResult adapter (policy is a security concern); platform-engineer owns the Terraform adapter (substrate translation). No overlap.
|
||||
- `backend-engineer` vs `platform-engineer` over `schemas/ir.schema.json` + `schemas/stack.schema.json`: platform-engineer owns the IR (substrate-agnostic but infra-shaped); backend-engineer owns the contract schema and the contract→IR resolution. Co-authoring is expected; conflict goes to lead-developer.
|
||||
- `backend-engineer` vs `security-engineer` over `core/confidence_signal.py`: security-engineer owns the severity→penalty mapping + critical-override semantics; backend-engineer owns the 6-input weighted sum + per-env thresholds. Co-owned; conflicts go to lead-developer.
|
||||
- `platform-engineer` vs `security-engineer` over `adapters/terraform/policy/**`: security-engineer owns the Checkov→PolicyCheckResult adapter + custom rules + the Wiz/Kyverno adapters (policy is a security concern); platform-engineer owns the Terraform adapter (substrate translation). No overlap.
|
||||
- `lambda-engineer` vs `platform-engineer` over `terraform/platform/main.tf`: lambda-engineer owns the Lambda + DynamoDB + Secrets Manager definitions; platform-engineer reviews the Terraform structure + state backend. Co-authoring expected; conflicts go to lead-developer.
|
||||
- `backend-engineer` vs `lambda-engineer` over `core/lambda/contract_ingestor.py` vs `scripts/run_platform.sh` + `.github/workflows/deploy.yml` error-report step: lambda-engineer owns the Lambda handler; backend-engineer owns the workflow step that invokes it. The interface (the JSON payload) is co-authored; conflicts go to lead-developer.
|
||||
- `lead-developer` vs any: lead-developer owns `.ciagent/**` + `docs/**` meta + verification scripts; persona engineers do not edit CIAgent metadata or the vision/architecture source docs.
|
||||
|
||||
## Territory enforcement mode
|
||||
|
||||
`warn` — config.json has no `personas.territory_enforcement` field, so the
|
||||
default per execute.md is `warn`. Cross-territory edits are logged in the
|
||||
commit message but do not fail the task. The spike's small scope means
|
||||
co-authoring across territories is likely; `warn` keeps it frictionless.
|
||||
commit message but do not fail the task. v1.7's broad scope means
|
||||
co-authoring across territories is likely (e.g. lambda + platform on
|
||||
`terraform/platform/main.tf`); `warn` keeps it frictionless.
|
||||
+217
-30
@@ -1,41 +1,228 @@
|
||||
---
|
||||
phase: 16
|
||||
name: v1.2-capstone-e2e
|
||||
milestone: v1.2
|
||||
requirements: [REQ-35]
|
||||
type: feat/verify
|
||||
branch: phase/16-v1.2-capstone-e2e
|
||||
phase: 28-38
|
||||
name: v1.8-p1-remediation-uptime-standards-encryption-decommission-docs
|
||||
milestone: v1.8
|
||||
requirements: [REQ-76, REQ-77, REQ-78, REQ-79, REQ-80, REQ-81, REQ-82, REQ-83, REQ-84, REQ-85, REQ-86, REQ-87, REQ-88, REQ-89, REQ-90, REQ-91, REQ-92, REQ-93, REQ-94, REQ-95, REQ-96, REQ-97, REQ-98, REQ-99]
|
||||
type: fix/feat/docs
|
||||
---
|
||||
|
||||
# Phase 16 — v1.2-capstone-e2e (v1.2) PLAN
|
||||
# ACDL v1.8 — Phase Plans
|
||||
|
||||
## Goal
|
||||
> Milestone: v1.8. Planner: ci-planner. Status: active.
|
||||
> 11 phases (28–38), 24 requirements (REQ-76..99).
|
||||
|
||||
End-to-end verification of the v1.2 platform: consumer commit → pipeline →
|
||||
`terraform apply` (dev) → live ECS service → evidence event → timeline. The
|
||||
`terraform apply` is blocked by the IAM P0 (Phase 15); Phase 16 ships the
|
||||
capstone verification of everything *up to* the apply + documents the
|
||||
operator's unblock step. After the operator pushes the policy, the apply +
|
||||
HTTP 200 check complete REQ-33/35.
|
||||
## Phase 28 — adapter-waf-and-resolver-outputs
|
||||
|
||||
## Tasks
|
||||
**Requirements:** REQ-76 (WAF nested rules + default_action), REQ-77 (L2 outputs resolution)
|
||||
**Personas:** platform-engineer (lead), backend-engineer
|
||||
**Type:** fix
|
||||
|
||||
### T-16.1 — Capstone verify script
|
||||
`scripts/verify_phase16.sh` runs the full v1.2 platform flow (consumer
|
||||
content → contract → IR → adapter → terraform validate + plan) + verifies
|
||||
the v1.1 regression + the NFR improvements (run_platform.sh, IAM policy
|
||||
expansion, P1-1 redaction) + the documentation (README accuracy). The
|
||||
`terraform apply` + HTTP 200 check are documented as the operator's
|
||||
post-unblock step.
|
||||
### Tasks (Wave 1 — sequential):
|
||||
|
||||
### T-16.2 — Capstone evidence event
|
||||
Write a `MILESTONE_CAPSTONE_VERIFIED` evidence event to the outbox (the
|
||||
v1.2 platform is verified up to the IAM-blocked apply).
|
||||
1. **platform-engineer:** Fix WAF `rules` emission in `adapters/terraform/adapter.py:346-348` — replace `rules = {_value_expr(...)}` with nested `rules { ... }` block emission per rule. Read `inputs.get("default_action")` (line 334) and emit `allow {}` / `block {}` based on input (default `allow` if absent).
|
||||
2. **backend-engineer:** Implement L2 composition `outputs[]` processing in `core/contract_resolver.py` `resolve_l2()` — after building `resources` (line 232), parse `composition.get("outputs", [])`, resolve source via `child_outputs`, build `stack_instance["outputs"]` dict.
|
||||
3. **platform-engineer:** Extend `adapter.py` `adapt()` to emit `output "<outName>" { value = <ref> }` blocks from `stack_instance.get("outputs", {})`.
|
||||
4. **platform-engineer:** Add tests to `tests/test_adapter.py` (WAF custom rules, default_action block, output blocks) + `tests/test_contract_resolver.py` (L2 outputs for static-assets).
|
||||
|
||||
### T-16.3 — Phase 16 README update
|
||||
Update README to reflect the v1.2 status (Phase 15 partial, Phase 16
|
||||
capstone, the IAM unblock step).
|
||||
### Must-haves:
|
||||
- WAF with custom rules emits `rules {` blocks, not `rules = [`
|
||||
- WAF `default_action: block` emits `block {}`
|
||||
- L2 resolution yields `stack.outputs.*`
|
||||
- Adapter emits `output` blocks
|
||||
- `pytest` passes (275 → ~285)
|
||||
|
||||
## Ship
|
||||
---
|
||||
|
||||
Merge → `main` (--no-ff). Tag `v1.2.6`.
|
||||
## Phase 29 — ssm-kms-and-invoke-policy
|
||||
|
||||
**Requirements:** REQ-78 (SSM fail-loud), REQ-79 (Terraform-rendered invoke policy)
|
||||
**Personas:** backend-engineer (lead), lambda-engineer, security-engineer
|
||||
**Type:** fix
|
||||
|
||||
### Tasks (Wave 1):
|
||||
|
||||
1. **backend-engineer:** Change `core/output_publisher.py:54-55` `_kms_key_id()` — raise `RuntimeError` when `ACDL_KMS_KEY_ID` unset; add `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch.
|
||||
2. **lambda-engineer:** Convert `terraform/platform/consumer_invoke_policy.json` to Terraform-rendered template — add `terraform/platform/variables.tf` with `data "aws_caller_identity" "current" {}` + `templatestring` or `replace()` for account ID injection.
|
||||
3. **backend-engineer:** Add `tests/test_output_publisher.py` cases: `test_kms_unset_raises`, `test_kms_unset_allow_default_kms`. Add `tests/test_invoke_policy.py` asserting rendered policy has no `000000000000`.
|
||||
|
||||
### Must-haves:
|
||||
- SSM raises RuntimeError without CMK; escape hatch works
|
||||
- Rendered invoke policy has live account ID
|
||||
- `pytest` passes (~290)
|
||||
|
||||
---
|
||||
|
||||
## Phase 30 — run-platform-isolation-and-api-portability
|
||||
|
||||
**Requirements:** REQ-80 (temp dir), REQ-81 (forge-agnostic URLs), REQ-82 (static-key override)
|
||||
**Personas:** backend-engineer (lead), lambda-engineer
|
||||
**Type:** fix
|
||||
|
||||
### Tasks (Wave 1 — parallel):
|
||||
|
||||
1. **backend-engineer:** Change `scripts/run_platform.sh:122` adapter target from `terraform/spike` to `$WORK/tf`. Update all downstream references. Remove committed `terraform/spike/*.tf`. Update `tests/test_pipeline.py`. Update deploy.yml artifact upload path.
|
||||
2. **lambda-engineer:** Add `_github_api_base()` + `_forge_type()` to `core/lambda/contract_ingestor.py`. Replace hardcoded URLs at lines 109, 149, 163. Add `tests/test_contract_ingestor.py` Gitea base URL test.
|
||||
3. **backend-engineer:** Restructure `configure-aws-credentials` step in both deploy workflows (byte-identical) — single conditional step with `access-key`/`secret-key` inputs when static key present. Update `tests/test_pipeline_contract.py`.
|
||||
|
||||
### Must-haves:
|
||||
- `run_platform.sh --check-only` writes to temp dir
|
||||
- `contract_ingestor.py` uses `GITHUB_API_BASE`
|
||||
- Deploy workflow static-key override wired
|
||||
- Both deploy workflows byte-identical
|
||||
- `pytest` passes (~295)
|
||||
|
||||
---
|
||||
|
||||
## Phase 31 — encryption-by-default-and-per-stack-cmk
|
||||
|
||||
**Requirements:** REQ-83 (kms-key primitive), REQ-84 (encryption NFRs on all primitives), REQ-85 (L2 CMK wiring)
|
||||
**Personas:** platform-engineer (lead), security-engineer
|
||||
**Type:** feat
|
||||
|
||||
### Tasks (Wave 1 — kms-key primitive + adapter):
|
||||
1. **platform-engineer:** Create `modules/l1/kms-key/` with `interface.json` (type `aws:kms:key`, inputs: description/region/deletion_window_days, outputs: kms_key_arn/kms_key_id, NFRs: enable_rotation default true, deletion_protection default true) + `instance.json` + `README.md` + `examples/`.
|
||||
2. **platform-engineer:** Add `aws:kms:key → aws_kms_key` + `aws:kms:alias → aws_kms_alias` to adapter TYPE_MAP. Emit `enable_key_rotation = true` + alias.
|
||||
|
||||
### Tasks (Wave 2 — encryption NFRs on all primitives, after Wave 1):
|
||||
3. **platform-engineer:** Add `encryption_enabled` NFR (default true) + `kms_key_arn` input to every L1 `interface.json` (s3, rds, ecr, ecs-service, ecs-cluster, alb, cloudfront, waf, vpc, iam-role). Update adapter to emit encryption blocks referencing the CMK when `kms_key_arn` is provided; managed KMS fallback with stderr warning when not.
|
||||
4. **platform-engineer:** Update both L2 `composition.json` files — add `kms-key` child + wires connecting `kms_key_arn` output to each child's `kms_key_arn` input.
|
||||
5. **platform-engineer:** Add `tests/test_encryption.py` — assert every primitive has encryption NFRs; assert adapter emits encryption blocks; assert L2 wires CMK; assert `enable_key_rotation = true`.
|
||||
|
||||
### Must-haves:
|
||||
- kms-key primitive exists + registered
|
||||
- All primitives have `encryption_enabled` NFR + `kms_key_arn` input
|
||||
- L2 modules wire per-stack CMK
|
||||
- Adapter emits encryption blocks
|
||||
- `pytest` passes (~310)
|
||||
|
||||
---
|
||||
|
||||
## Phase 32 — deletion-protection-by-default-and-l2-feature-flag
|
||||
|
||||
**Requirements:** REQ-86 (deletion_protection NFR on all primitives), REQ-87 (L2 feature flag)
|
||||
**Personas:** platform-engineer (lead), backend-engineer
|
||||
**Type:** feat
|
||||
|
||||
### Tasks (Wave 1):
|
||||
1. **platform-engineer:** Add `deletion_protection` NFR (boolean, default true) to every L1 `interface.json` (rds already has it). Update adapter to emit `lifecycle { prevent_destroy = true }` when NFR is true; omit when false. RDS gets BOTH `deletion_protection` arg + `prevent_destroy` lifecycle.
|
||||
2. **backend-engineer:** Add `features` object support to `schemas/stack.schema.json` (optional `features.deletion_protection`). Update `core/contract_resolver.py` `resolve_l2()` to propagate `features.deletion_protection` to each child's `deletion_protection` NFR. Add `inputs.deletion_protection` to `schemas/contract.schema.json` (optional boolean).
|
||||
3. **platform-engineer:** Add `tests/test_deletion_protection.py` — assert every primitive has the NFR; assert adapter emits `prevent_destroy`; assert L2 feature flag propagation.
|
||||
|
||||
### Must-haves:
|
||||
- Every primitive has `deletion_protection` NFR (default true)
|
||||
- Adapter emits `prevent_destroy = true` when true
|
||||
- L2 feature flag propagates
|
||||
- `pytest` passes (~320)
|
||||
|
||||
---
|
||||
|
||||
## Phase 33 — uptime-kuma-primitive
|
||||
|
||||
**Requirements:** REQ-88 (uptime primitive), REQ-89 (deployed by default after L2), REQ-90 (feature flag), REQ-91 (pipeline stage)
|
||||
**Personas:** platform-engineer (lead), backend-engineer
|
||||
**Type:** feat
|
||||
|
||||
### Tasks (Wave 1 — primitive + adapter):
|
||||
1. **platform-engineer:** Create `modules/l1/uptime/` with `interface.json` (type `aws:ecs:uptime-service`, inputs: container_image/region/monitored_endpoints/static_checks/alert_channels/feature_flag_enabled/cpu/memory, outputs: uptime_url/service_arn/task_definition_arn, NFRs: deletion_protection/encryption_enabled) + `instance.json` + `README.md` + `examples/simple.yaml` + `examples/complex.yaml`.
|
||||
2. **platform-engineer:** Add `aws:ecs:uptime-service` to adapter TYPE_MAP. Emit ECS Fargate task + service + ALB + listener + EFS volume + CloudWatch log group. When `feature_flag_enabled=false`, emit NO resources. Register in `registry.json`.
|
||||
|
||||
### Tasks (Wave 2 — pipeline + script, after Wave 1):
|
||||
3. **backend-engineer:** Add `deploy-uptime` stage to `pipelines/deploy.yaml` (after `publish-outputs`). Update both deploy workflows (byte-identical) with the stage. Add `scripts/seed_uptime_monitors.py` for post-deploy monitor seeding via uptime-kuma API.
|
||||
4. **backend-engineer:** Update `scripts/run_platform.sh` — add `deploy-uptime` step: read L2 stack outputs, construct synthetic uptime contract with `monitored_endpoints` from outputs, run second terraform apply with separate state (`$WORK/uptime-tf/`), publish uptime URL via PR comment. Skip when `inputs.uptime_enabled=false`.
|
||||
5. **backend-engineer:** Add `tests/test_uptime_primitive.py` — validate interface; assert adapter emits ECS service when flag=true; assert no resources when flag=false; assert `deploy-uptime` stage in pipeline contract.
|
||||
|
||||
### Must-haves:
|
||||
- Uptime primitive exists with feature flag + alert channels
|
||||
- Deployed by default after L2 (separate state)
|
||||
- Uptime URL published via PR comment
|
||||
- Feature flag disables deployment
|
||||
- `deploy-uptime` stage in deploy contract + byte-identical workflows
|
||||
- `pytest` passes (~335)
|
||||
|
||||
---
|
||||
|
||||
## Phase 34 — decommission-alias-and-cmdb-validation
|
||||
|
||||
**Requirements:** REQ-92 (decommission mode), REQ-93 (CMDB validation), REQ-94 (consumer docs)
|
||||
**Personas:** backend-engineer (lead), lambda-engineer, security-engineer
|
||||
**Type:** feat
|
||||
|
||||
### Tasks (Wave 1 — CMDB + Lambda, parallel):
|
||||
1. **lambda-engineer:** Add `acdl-change-requests` DynamoDB table to `terraform/platform/main.tf` (PK changeRequestId, SK submittedAt, SSE via CMK, PITR). Add `validate_change_request` action to `core/lambda/contract_ingestor.py` — query table, assert status=approved + consumerRepo match, return CR details or 403.
|
||||
2. **backend-engineer:** Add `decommission_transform(stack_instance)` to `core/contract_resolver.py` — zero all counts (desired_count=0 for ECS, etc.).
|
||||
|
||||
### Tasks (Wave 2 — pipeline + docs, after Wave 1):
|
||||
3. **backend-engineer:** Add `mode: decommission` to deploy workflow inputs. Add decommission stages to `pipelines/deploy.yaml`: validate-change-request → disable-deletion-protection (HITL SRE gate via GitHub environment) → zero-counts (second HITL SRE gate) → confirm-decommission. Update both deploy workflows (byte-identical).
|
||||
4. **backend-engineer:** Update `docs/CONSUMER_GUIDE.md` with "Decommissioning a stack" section (request CR, trigger decommission, HITL gates, what happens).
|
||||
5. **backend-engineer:** Add `tests/test_decommission.py` — assert `decommission_transform` zeroes counts; assert `validate_change_request` rejects invalid CRs; assert decommission stages in pipeline contract.
|
||||
|
||||
### Must-haves:
|
||||
- Decommission mode on existing deploy pipeline
|
||||
- 2-step with HITL SRE gates
|
||||
- CR ID validated against DynamoDB CMDB
|
||||
- Documented in consumer guide
|
||||
- `pytest` passes (~345)
|
||||
|
||||
---
|
||||
|
||||
## Phase 35 — module-engineering-standards
|
||||
|
||||
**Requirements:** REQ-95 (STANDARDS.md), REQ-96 (catalog fix + template update)
|
||||
**Personas:** lead-developer (lead), platform-engineer, backend-engineer
|
||||
**Type:** docs + refactor
|
||||
|
||||
### Tasks (Wave 1):
|
||||
1. **lead-developer:** Author `modules/STANDARDS.md` — comprehensive L1+L2 authoring + review standards (scanned from current modules per RESEARCH TARGET 6): required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, multi-resource pattern, adapter extension pattern, code review checklist.
|
||||
2. **lead-developer:** Fix `modules/README.md` catalog index — add rds + uptime + kms-key to Primitives table. Update `modules/README-TEMPLATE.md` — add `## NFRs` section.
|
||||
3. **backend-engineer:** Add `tests/test_module_standards.py` — automated enforcement: every L1 has `deletion_protection` + `encryption_enabled` NFRs; every L2 has valid structure; every module registered; every module has README + examples.
|
||||
|
||||
### Must-haves:
|
||||
- `modules/STANDARDS.md` exists with L1+L2 standards
|
||||
- Catalog index includes all primitives
|
||||
- Template has NFRs section
|
||||
- Automated standards test passes
|
||||
- `pytest` passes (~355)
|
||||
|
||||
---
|
||||
|
||||
## Phase 36 — schemas-adapters-pipelines-readmes
|
||||
|
||||
**Requirements:** REQ-97 (schemas README), REQ-98 (pipelines README), REQ-99 (adapters README)
|
||||
**Personas:** lead-developer (lead), backend-engineer, platform-engineer
|
||||
**Type:** docs
|
||||
|
||||
### Tasks (Wave 1 — parallel):
|
||||
1. **lead-developer:** Author `schemas/README.md` — how to write schemas, wire into platform, test in CI, dependencies, existing catalog.
|
||||
2. **lead-developer:** Author `pipelines/README.md` — how to write pipeline contracts, wire into workflows, test, dependencies, catalog.
|
||||
3. **lead-developer:** Author `adapters/README.md` — how to write adapters, wire into platform, test, dependencies, catalog.
|
||||
4. **backend-engineer:** Add `tests/test_docs_coverage.py` — assert all 3 READMEs exist + contain required sections.
|
||||
|
||||
### Must-haves:
|
||||
- All 3 READMEs exist with comprehensive documentation
|
||||
- CI validates presence
|
||||
- `pytest` passes (~358)
|
||||
|
||||
---
|
||||
|
||||
## Phase 37 — verify
|
||||
|
||||
**Personas:** lead-developer (lead), all personas
|
||||
**Type:** verify
|
||||
|
||||
### Tasks:
|
||||
1. Structural: all new files present.
|
||||
2. Behavioral: `pytest` passes (~358); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
3. Security: no secrets; CMK enforced; no placeholder account IDs; deletion protection on by default.
|
||||
4. Quality: each P1 fix + each new feature has a dedicated test.
|
||||
|
||||
---
|
||||
|
||||
## Phase 38 — review-audit-complete
|
||||
|
||||
**Personas:** lead-developer (lead), all personas
|
||||
**Type:** review + audit + complete
|
||||
|
||||
### Tasks:
|
||||
1. Review: 0 new P0/P1; all P1-3..P1-9 + S1 resolved; 3 new requirements delivered.
|
||||
2. Audit: reconstruction, file discipline, branch hygiene, commit discipline.
|
||||
3. Complete: update REQUIREMENTS.md (REQ-76..99), ROADMAP.md, PROJECT.md. Tag `v1.8.0`. Update floating `v1.8` + `v1` tags. Bump `uses:` to `@v1.8`.
|
||||
+177
-7
@@ -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.**
|
||||
@@ -167,6 +167,135 @@ Three scope axes:
|
||||
Milestone COMPLETE gate: review → ship `v1.4.1` (feature milestone, next
|
||||
minor per ship.md — v1.3 shipped `v1.3.2`) → audit.
|
||||
|
||||
## Objective for Milestone v1.7 (complete)
|
||||
|
||||
Production platform + contract ingestion + pipeline maturation. The v1.6
|
||||
milestone left the platform documented and environments-aware; v1.7 took it
|
||||
to a production-grade platform. 12 user-directed scope axes (2026-07-22):
|
||||
|
||||
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
|
||||
new `cloudfront` primitive + a `waf` primitive (D-049: S3 + CloudFront
|
||||
OAC + WAF; Route53/ACM are domain-dependent and deferred to documented
|
||||
extension points).
|
||||
3. **DX-friendly deploy outputs** (D-050): SSM Parameter Store (KMS-encrypted
|
||||
`SecureString`) for runtime-injectable values + GitHub PR comment / job
|
||||
summary for human-readable connection strings. No raw secrets in logs.
|
||||
4. **Central deploy pipeline error reporting** via the platform Lambda
|
||||
`report_error` action (D-055): the Lambda creates a GitHub issue on the
|
||||
platform repo. The consumer's onboarding-granted Lambda-invoke permission
|
||||
is the only grant needed — uniform pathway, no separate GitHub
|
||||
`issues: write` on the consumer side. Gitea is excluded (only the CIAgent
|
||||
uses it).
|
||||
5. **PR comments after every successful stage** so developers always know
|
||||
where they stand.
|
||||
6. **Three platform pipelines**: (1) platform-test (PR, unit + integration +
|
||||
schema-validation); (2) primitives-plan (PR, plan-only for all L1
|
||||
primitives); (3) patterns-plan (PR, plan-only for all L2 modules).
|
||||
7. **Release job** on merge to `main`: computes MAJOR.MINOR.PATCH semver,
|
||||
creates the tag, then updates (force-moves) or creates the MAJOR.MINOR +
|
||||
MAJOR floating tags (D-057). Consumers on `@v1` or `@v1.6` receive updates
|
||||
depending on their pinned version.
|
||||
8. **Platform Lambda** for one-way consumer→platform communication
|
||||
(contracts). Onboarding grants the consumer repo's environment the right
|
||||
to trigger the Lambda (cross-account IAM). The Lambda ingests contracts
|
||||
and stores them in a DynamoDB table `acdl-contracts` (D-051) for
|
||||
historical reference, impact analysis, CMDB-style application-state
|
||||
queries, and pattern detection. The IAM policy reflects cross-account
|
||||
invocation.
|
||||
9. **Tagging standards** in policy/compliance checks (D-054): a required-tag
|
||||
set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`)
|
||||
enforced by a Checkov custom YAML rule. Closes the D-043 deferral (the
|
||||
SKIPPED `ACDL_TAG_NAMING` placeholder becomes a real check).
|
||||
10. **Wiz adapter** for security checks (D-052): a stub + schema path that
|
||||
translates Wiz API issues → `PolicyCheckResult` records, degrading
|
||||
gracefully when unconfigured. Matches the Checkov adapter pattern.
|
||||
11. **Kyverno adapter** for compliance/security checks (D-053): a
|
||||
K8s-native policy adapter that translates Kyverno `PolicyReport` results
|
||||
→ `PolicyCheckResult` records. Ready but inactive for Terraform-only
|
||||
stacks (the platform emits Terraform, not K8s manifests); it activates
|
||||
when the GitOps reconciler (roadmap) emits K8s manifests.
|
||||
12. **Remove the legacy consumer-repos directory** and add validated per-module examples
|
||||
(D-058: `modules/<name>/examples/` with `simple.yaml` + `complex.yaml`
|
||||
validated in CI) + a new RDS primitive demonstrating multi-engine
|
||||
variation (D-059).
|
||||
|
||||
## Milestone v1.7 Phases
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 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. |
|
||||
| 26 | platform-pipelines-and-release-automation | 3 platform pipelines (platform-test, primitives-plan, patterns-plan). Release job with semver + MAJOR.MINOR/MAJOR tag updates (D-057). |
|
||||
| 27 | remove-legacy-consumer-repos-and-module-documentation-examples | Delete the legacy consumer-repos directory. RDS primitive (D-059). Validated per-module examples (D-058). Docs updates. |
|
||||
|
||||
Milestone COMPLETE gate: review → ship `v1.7.0` (feature milestone, next
|
||||
minor per ship.md — v1.6 shipped `v1.6.0`) → audit.
|
||||
|
||||
## Objective for Milestone v1.8 (active)
|
||||
|
||||
P1 remediation + uptime monitoring + engineering standards + encryption
|
||||
and deletion-protection by default + decommission alias + documentation.
|
||||
The v1.7 milestone shipped production platform + contract ingestion but
|
||||
left 8 P1 issues flagged for post-hoc review. v1.8 clears all of them
|
||||
AND delivers three user-directed feature/NFR tracks (2026-07-22):
|
||||
|
||||
**Track 1 — P1 Remediation (Phases 28–30):**
|
||||
Clear all 8 pending P1 issues from v1.5/v1.6/v1.7 verify reviews:
|
||||
- P1-3: SSM uses AWS-managed key silently → fail loud without CMK config
|
||||
- P1-4: WAF custom rules emit invalid HCL (attribute vs block syntax)
|
||||
- P1-5: WAF default_action input silently ignored
|
||||
- P1-6: consumer_invoke_policy.json has placeholder account ID
|
||||
- P1-7: L2 composition outputs section not implemented in resolver
|
||||
- P1-8: terraform/spike/*.tf overwritten by run_platform.sh (state
|
||||
contamination)
|
||||
- P1-9: GitHub API URLs hardcoded in contract_ingestor.py (Gitea fails
|
||||
silently)
|
||||
- S1: Deploy workflow static-key override not wired (passes ACDL_AWS_*
|
||||
env vars to configure-aws-credentials which reads AWS_*/its own inputs)
|
||||
|
||||
**Track 2 — Encryption + Deletion Protection by Default (Phases 31–32):**
|
||||
All primitives encrypted by default (CMK priority + SSE, managed KMS
|
||||
fallback). Per-stack CMK (one key per L2 deployment, 90-day rotation,
|
||||
no shared keys). Deletion protection on by default for every primitive.
|
||||
L2 modules expose a feature flag to turn off deletion protection. A
|
||||
decommission alias uses a 2-step pipeline (disable deletion protection
|
||||
→ zero counts → destroy) with HITL SRE gates and CMDB-validated change
|
||||
request ID.
|
||||
|
||||
**Track 3 — Uptime + Standards + Docs (Phases 33–36):**
|
||||
A new uptime-kuma primitive (ECS Fargate) deployed by default after any
|
||||
L2 module deploy (separate terraform state), with a feature flag to
|
||||
disable. Monitored endpoints passed from L2 outputs. Alert channels
|
||||
(Teams/email/SMS/GitHub issues). The uptime URL published to consumers
|
||||
via PR comments. Engineering standards for L1 + L2 module authoring
|
||||
(scanned from current modules, stored in modules/). READMEs for
|
||||
schemas/, adapters/, pipelines/ paths documenting how to write, wire,
|
||||
and test each.
|
||||
|
||||
## Milestone v1.8 Phases
|
||||
|
||||
| Phase | Name | Goal |
|
||||
|-------|------|------|
|
||||
| 28 | adapter-waf-and-resolver-outputs | Fix WAF HCL emission (nested rules blocks + default_action input) + implement L2 composition outputs in resolver + adapter output blocks. P1-4, P1-5, P1-7. |
|
||||
| 29 | ssm-kms-and-invoke-policy | SSM publisher fails loud without CMK (escape hatch for local) + Terraform-rendered consumer_invoke_policy (no placeholder account ID). P1-3, P1-6. |
|
||||
| 30 | run-platform-isolation-and-api-portability | Adapter output to per-run temp dir (remove committed spike .tf) + forge-agnostic API URLs + deploy.yml static-key override wired. P1-8, P1-9, S1. |
|
||||
| 31 | encryption-by-default-and-per-stack-cmk | KMS-key primitive + per-stack CMK wired in L2 modules + encryption NFRs on all primitives + managed KMS fallback. |
|
||||
| 32 | deletion-protection-by-default-and-l2-feature-flag | Deletion protection NFR on all primitives (default true) + L2 feature flag + contract schema update. |
|
||||
| 33 | uptime-kuma-primitive | Uptime L1 primitive (ECS Fargate, feature flag, monitored endpoints, alert channels) + deploy-uptime pipeline stage (separate state) + URL published via PR comment. |
|
||||
| 34 | decommission-alias-and-cmdb-validation | Decommission mode on deploy pipeline (2-step: disable deletion protection → zero counts, HITL SRE gates) + DynamoDB CMDB validation + consumer guide docs. |
|
||||
| 35 | module-engineering-standards | modules/STANDARDS.md (L1+L2 authoring + review standards scanned from current modules) + catalog index fix + template update + automated standards test. |
|
||||
| 36 | schemas-adapters-pipelines-readmes | schemas/README.md + pipelines/README.md + adapters/README.md (how to write, wire, test, dependencies). |
|
||||
| 37 | verify | 4-layer verification of all v1.8 phases. |
|
||||
| 38 | review-audit-complete | Multi-persona review + audit + milestone completion (tag v1.8.0). |
|
||||
|
||||
Milestone COMPLETE gate: review → ship `v1.8.0` (feature milestone, next
|
||||
minor per run.md — v1.7 shipped `v1.7.0`) → audit.
|
||||
|
||||
## Requirements
|
||||
|
||||
### v1.0 (Prior milestone — the demo)
|
||||
@@ -193,7 +322,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.
|
||||
@@ -223,7 +352,7 @@ New requirements REQ-29..REQ-35 — see `REQUIREMENTS.md` §v1.2. Summary:
|
||||
- **REQ-35:** End-to-end verification — consumer commit → live ECS service
|
||||
(HTTP 200) → evidence event → timeline.
|
||||
|
||||
### v1.4 (Active milestone — central pipeline contract + shell reproducibility + streaming)
|
||||
### v1.4 (Prior milestone — central pipeline contract + shell reproducibility + streaming)
|
||||
|
||||
New requirements REQ-43..REQ-45 — see `REQUIREMENTS.md` §v1.4. Summary:
|
||||
|
||||
@@ -279,7 +408,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 |
|
||||
@@ -292,6 +421,47 @@ decisions:
|
||||
| D-046 | `act_runner` → `gitea-runner` rename: Phase 07 updates docs to use the current name `gitea-runner` (renamed 2026-04 in gitea/runner#850). | RESEARCH TARGET 1 + R-4: naming drift between v1.0 docs and the current runner. | Docs reflect the current binary name |
|
||||
| D-047 | v1.2 carries forward the D-039 per-run-rotated-key waiver. Real OIDC federation remains deferred to v1.3+, blocked on go-gitea/gitea#36988 (re-checked 2026-07-21: still **open**, last updated 2026-05-27, not merged). | §12.5 forbids long-lived creds; the Gitea Actions OIDC provider is still not merged. The waiver continues to satisfy §12.5's *intent* (no *persistently* long-lived key) for v1.2: `scripts/rotate_spike_key.sh` rotates the key, and Phase 12 tightens the IAM scoping + rotation hygiene. | v1.2 achieves `terraform apply` against AWS without a persistently long-lived key; real OIDC is a v1.3+ deliverable. |
|
||||
|
||||
## Key Decisions (v1.8)
|
||||
|
||||
Resolved at the CLARIFY stage (full autonomy — all within locked
|
||||
constraints or user-directed scope). New v1.8 decisions:
|
||||
|
||||
| ID | Decision | Rationale | Outcome |
|
||||
|----|----------|-----------|---------|
|
||||
| D-061 | Fold all 3 new requirements into v1.8 alongside P1 fixes. | User chose single milestone. v1.8 becomes a feature milestone (ship tag v1.8.0, minor bump). | 11 phases (28–38) in one milestone. |
|
||||
| D-062 | P1-3: SSM publisher fails loud (`RuntimeError`) when `ACDL_KMS_KEY_ID` unset. `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing. | User chose fail loud. Silent AWS-managed-key use is the security gap; callers must set the env. | Phase 29 implements fail-loud + escape hatch. |
|
||||
| D-063 | P1-6: `consumer_invoke_policy.json` rendered via Terraform `data.aws_caller_identity` + `templatestring` at apply time. | User chose Terraform-rendered. No committed account ID; no stale placeholder. | Phase 29 converts JSON to TF-rendered template. |
|
||||
| D-064 | P1-8: Remove committed `terraform/spike/*.tf` entirely; adapter emits to per-run temp dir. | User chose remove. Cleaner; no stale fixtures. | Phase 30 removes files + changes run_platform.sh target. |
|
||||
| D-065 | S1: Single conditional `configure-aws-credentials` step (OIDC when no static key, access-key/secret-key inputs when static key present). | User chose single conditional step. Cleaner workflow YAML. | Phase 30 restructures the deploy workflow step. |
|
||||
| D-066 | Uptime deployment target: ECS Fargate (reuse existing ecs-cluster + ecs-service + alb primitives). | User chose ECS Fargate. Most consistent with current platform; ALB gives a stable URL. | Phase 33 authors uptime primitive on ECS Fargate. |
|
||||
| D-067 | Uptime trigger: new `deploy-uptime` pipeline stage after `publish-outputs`. Separate terraform state (S3 key prefix `uptime/`). | User chose pipeline stage. Most integrated with existing flow. | Phase 33 adds the pipeline stage + separate state. |
|
||||
| D-068 | CMDB = DynamoDB `acdl-change-requests` table (PK changeRequestId, SK submittedAt). | User chose DynamoDB. Consistent with existing platform Lambda + DynamoDB pattern. | Phase 34 adds the table + `validate_change_request` Lambda action. |
|
||||
| D-069 | Encryption key granularity: per-stack CMK (one key per L2 deployment, tagged with acdl:owner + acdl:environment). | User chose per-stack. No shared keys across stacks; 90-day rotation at creation. | Phase 31 authors kms-key primitive + L2 wiring. |
|
||||
| D-070 | Decommission: new mode on the existing deploy pipeline (`mode: decommission`). 2-step with HITL SRE gates. | User chose existing pipeline with different behavior. Plan/apply to disable deletion protection (HITL SRE gate) → plan/apply with counts=0 (second HITL SRE gate). Documented in consumer guide. | Phase 34 adds decommission mode + HITL gates. |
|
||||
| D-071 | `uses:`/`ref:` bump from `@v1.6` to `@v1.8` at milestone COMPLETE. | Consumer-facing version tracks the last released MAJOR.MINOR. | Phase 38 bumps references + creates floating `v1.8` + `v1` tags. |
|
||||
| D-072 | Managed KMS fallback for standalone L1 deployments (no L2 CMK): adapter uses `alias/aws/<service>` with a stderr warning. `kms_key_arn` input is optional everywhere; `encryption_enabled` NFR defaults to true. | Requirement says "prioritize CMKs, fallback to managed KMS". Standalone L1s don't have a per-stack CMK. | Phase 31 implements fallback + warning. |
|
||||
|
||||
## Key Decisions (v1.7)
|
||||
|
||||
Resolved at the CLARIFY stage (full autonomy — all within locked constraints
|
||||
or user-directed scope). New v1.7 decisions:
|
||||
|
||||
| ID | Decision | Rationale | Outcome |
|
||||
|----|----------|-----------|---------|
|
||||
| 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. |
|
||||
| D-052 | Wiz adapter = stub + schema path (no live Wiz tenant in CI). | Matches the Checkov adapter pattern; typed interface, offline-testable, degrades gracefully when unconfigured (emits `WIZ_NOT_CONFIGURED` SKIPPED record). | Phase 23 authors `adapters/wiz/wiz_adapter.py`. |
|
||||
| D-053 | Kyverno adapter = K8s-native policy adapter translating `PolicyReport` results → `PolicyCheckResult`. Ready but inactive for Terraform-only stacks. | The platform emits Terraform, not K8s manifests. The adapter activates when the GitOps reconciler (roadmap) emits K8s manifests. Sample policies included as documentation. | Phase 23 authors `adapters/kyverno/kyverno_adapter.py` + sample policies. |
|
||||
| D-054 | Tagging standard = required-tag set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`) enforced by a Checkov custom YAML rule. | Closes the D-043 deferral (the SKIPPED `ACDL_TAG_NAMING` placeholder becomes a real check). Naming-convention regex deferred (brittle across AWS resource types). | Phase 23 authors `schemas/tagging-standard.json` + `adapters/terraform/policy/custom_rules/acdl_tagging.yaml`. |
|
||||
| D-055 | Error reporting = the platform Lambda `report_error` action creates a GitHub issue on the platform repo (`acdl/acdl`). Uniform communication pathway via the Lambda; the consumer's onboarding-granted Lambda-invoke permission is the only grant needed. No separate GitHub `issues: write` on the consumer side. Gitea is excluded (only the CIAgent uses it; platform engineers and consumers use GitHub). | Unifies requirements 4 + 8 around one mechanism. The Lambda holds a GitHub token (Secrets Manager) scoped to the platform repo. Idempotent (comments on existing open issue rather than duplicating). | Phase 24 prepares the action; Phase 25 implements it + wires the `if: failure()` workflow step. |
|
||||
| D-056 | Ship `v1.7.0`; bump `uses:`/`ref:` from `@v1.4` to `@v1.6`. | Consumer-facing version tracks the last released MAJOR.MINOR. Consumers on `@v1.4` stay on v1.4 behavior until they bump. | Phase 22 bumps the references. |
|
||||
| D-057 | The `uses:`/`ref:` bump + floating `v1.6`/`v1` tag creation happen in Phase 22 (pointing at `v1.6.0`), so the reference never points at a non-existent tag. The release job (Phase 26) owns ongoing tag updates. | Sequencing: if Phase 22 bumps `uses:` to `@v1.6` but the tag doesn't exist, the reference is temporarily broken. Creating the tag early (pointing at the last release) fixes this. | Phase 22 creates the floating tags; Phase 26's release job maintains them. |
|
||||
| D-058 | Module examples = separate validated files in `modules/<name>/examples/` (`simple.yaml` + `complex.yaml` + variation files), validated against `schemas/contract.schema.json` in the platform-test pipeline schema-validation stage. Each module's README `## Examples` section references + excerpts them. | Examples cannot drift from the schema silently. | Phase 27 authors the example files; Phase 26's platform-test pipeline validates them. |
|
||||
| D-059 | Add an RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) + a multi-engine example demonstrating the variation pattern. | Concrete demonstration of the multi-engine variation the requirement calls out. Adds one primitive + examples. | Phase 27 authors the primitive + adapter expansion + examples. |
|
||||
| D-060 | (Consolidated into D-058.) | — | — |
|
||||
|
||||
### Open-decision resolutions (Phase 07 deliverable — recorded here for traceability)
|
||||
|
||||
| ID | Question | Resolution |
|
||||
@@ -329,8 +499,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 |
|
||||
|
||||
+122
-6
@@ -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,19 +124,19 @@
|
||||
|
||||
### 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)
|
||||
|
||||
### Category: Internal-surface scrub
|
||||
- **REQ-52:** No consumer-facing documentation (README.md, docs/**, modules/**/README.md, contracts/**) references `.ciagent/` — it is local CIAgent metadata, never visible to platform engineers or consumers. The README repository-layout table has no `.ciagent/` row. No `.gitea/` references appear in consumer-facing docs (consumers use GitHub only); the README repository-layout table has no `.gitea/workflows/` row.
|
||||
- **REQ-53:** `acdl_platform/` is renamed to `platform/` across the directory, all imports in tests/scripts/pipelines/workflows, and all doc references. `grep -R "acdl_platform" .` (excluding `.ciagent/`, `demo/`, `.git/`) returns 0 hits. The test suite passes after the rename.
|
||||
- **REQ-53:** `acdl_platform/` is renamed to `core/` across the directory, all imports in tests/scripts/pipelines/workflows, and all doc references. (`platform/` was the original target but shadows Python's stdlib `platform` module — `core/` was chosen to stay importable.) `grep -R "acdl_platform" .` (excluding `.ciagent/`, `demo/`, `.git/`) returns 0 hits. The test suite passes after the rename.
|
||||
|
||||
### Category: Docs site restructure
|
||||
- **REQ-54:** `docs/` is restructured into a Jekyll-style GitHub Pages site: `docs/_config.yml`, `docs/index.md` (landing), `docs/modules/` (catalog + per-module Pages-friendly copies), `docs/contracts/index.md`, `docs/pipeline/index.md` + `docs/pipeline/versioning.md`, `docs/environments/index.md`, `docs/consumer-guide.md`, `docs/architecture.md` (consolidated from architecture.md + architecture-v1.0.md, current-architecture only), `docs/vision.md`. No `.ciagent/` links anywhere in `docs/`. Consumer-facing content (modules, contracts, pipeline, versioning) lives in Pages.
|
||||
@@ -154,6 +154,74 @@
|
||||
### Category: Environments concept + onboarding
|
||||
- **REQ-61:** The concept of platform-managed environments is introduced: consumers are not required to provide an AWS account, VPC, subnet, S3 state bucket, or runner key. `docs/environments/index.md` documents that a named environment is a platform-owned AWS account + network + state backend + IAM role surfaced to the consumer via ABAC, selected by name in the contract. The old README environments table (dev/qa/prod/dr) is removed completely. A minimal onboarding scaffold exists: `platform/environments/` with a sample `dev.json` + README, `platform/environment_check.py`, a wire-in at the top of `scripts/run_platform.sh`, a friendly first-run onboarding message when no environment is defined for the repo, and `tests/test_environment_check.py` covering the missing-env and present-env cases.
|
||||
|
||||
## v1.7 (Active — production platform + contract ingestion + pipeline maturation)
|
||||
|
||||
### Category: Rename + production-ready stack
|
||||
- **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`.
|
||||
|
||||
### Category: Tagging standards + security adapters
|
||||
- **REQ-65:** A required-tag set is defined in `schemas/tagging-standard.json` (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`). A Checkov custom YAML rule at `adapters/terraform/policy/custom_rules/acdl_tagging.yaml` fails (severity `medium`) when required tags are missing on taggable resources. `checkov_adapter.py` removes the `_emit_tag_naming_skipped()` placeholder (D-043 closure) and maps `ACDL_TAG_NAMING` as a real rule. `scripts/run_platform.sh` Step 5 passes `--external-checks-dir` to load the custom rule.
|
||||
- **REQ-66:** A Wiz adapter stub exists at `adapters/wiz/wiz_adapter.py` translating Wiz API issues → `PolicyCheckResult` records (`engine: "wiz"`, D-052). It degrades gracefully when unconfigured (emits a single `SKIPPED` `WIZ_NOT_CONFIGURED` record). `tests/test_wiz_adapter.py` passes offline with a fixture response. The pipeline invokes it optionally (Step 5b) when `WIZ_API_TOKEN` is set.
|
||||
- **REQ-67:** A Kyverno K8s-native adapter exists at `adapters/kyverno/kyverno_adapter.py` translating Kyverno `PolicyReport` results → `PolicyCheckResult` records (`engine: "kyverno"`, D-053). Sample policies exist at `adapters/kyverno/policies/` (disallow-privileged, require-labels, require-image-digests). `tests/test_kyverno_adapter.py` passes offline. The adapter is inactive for Terraform-only stacks (the platform emits Terraform, not K8s manifests); it is ready for the GitOps reconciler roadmap item. `schemas/policy_check_result.schema.json` engine enum includes `checkov | kyverno | opa | wiz`.
|
||||
|
||||
### Category: Platform Lambda + contract ingestion
|
||||
- **REQ-68:** A platform Lambda (`core/lambda/contract_ingestor.py`) is invoked via a Function URL (IAM auth) and accepts `{ consumerRepo, contractId, contract, environment, action }`. It writes contracts to a DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via a customer-managed CMK, point-in-time recovery) (D-051). `terraform/platform/main.tf` defines the table, Lambda, Function URL, KMS key, Secrets Manager secret (`acdl/github-token`), and Lambda execution role. `terraform/platform/consumer_invoke_policy.json` grants the consumer's deploy role `lambda:InvokeFunctionUrl` on the Lambda ARN, scoped via ABAC (cross-account). Onboarding grants the Lambda-invoke permission; `docs/environments/index.md` documents this. `tests/test_contract_ingestor.py` passes offline (moto-mocked DynamoDB).
|
||||
|
||||
### Category: Deploy outputs + error reporting + stage comments
|
||||
- **REQ-69:** `scripts/run_platform.sh` has a `publish-outputs` step (after apply) that writes deploy outputs to SSM Parameter Store as `SecureString` (KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values, and a `comment-outputs` step that posts a structured GitHub PR comment / job summary with human-readable connection strings (D-050). `core/output_publisher.py` implements the SSM write + GitHub comment formatting. `tests/test_output_publisher.py` passes offline (moto + mocked GitHub API). `pipelines/deploy.yaml` + both deploy workflow YAMLs declare the new stages (byte-identical).
|
||||
- **REQ-70:** The Lambda `report_error` action (`core/lambda/contract_ingestor.py`) creates a GitHub issue on the platform repo (`acdl/acdl`) via the GitHub API using a token from Secrets Manager (D-055). Idempotent (comments on an existing open issue rather than duplicating). `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) have an `if: failure()` error-report step invoking the Lambda via `aws lambda invoke-function-url` (SigV4-signed). Gitea is excluded (only the CIAgent uses it; platform engineers and consumers use GitHub).
|
||||
- **REQ-71:** `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) post a PR comment after every successful pipeline stage (validate-contract, resolve-stack, plan, checkov, confidence, apply, publish-outputs) via `scripts/post_stage_comment.sh` (uses `GITHUB_TOKEN` + `gh api`; no-op when not in a PR context). The comment includes the stage name, status (pass), and key metrics (plan counts, confidence score, outputs published).
|
||||
|
||||
### Category: Platform pipelines + release automation
|
||||
- **REQ-72:** Three platform pipelines exist: (1) `.github/workflows/platform-test.yml` (PR, stages: lint, unit-test, integration-test — runs `run_platform.sh --check-only` for every sample contract, schema-validation — validates all `schemas/*.json` + `modules/**/interface.json` + `modules/**/composition.json` + `modules/<name>/examples/*.yaml` against their schemas); (2) `.github/workflows/primitives-plan.yml` (PR, plan-only for all L1 primitives via matrix, `scripts/run_primitive_plan.sh`); (3) `.github/workflows/patterns-plan.yml` (PR, plan-only for all L2 modules via matrix, `scripts/run_pattern_plan.sh`).
|
||||
- **REQ-73:** `.github/workflows/release.yml` runs on merge to `main`, computes the next semver (PATCH per phase, MINOR on milestone COMPLETE), creates the MAJOR.MINOR.PATCH tag, force-moves the MAJOR.MINOR + MAJOR floating tags, and creates a GitHub release with an auto-generated body (D-057). `tests/test_release_logic.py` passes (unit test the semver computation + tag-update logic with a mocked `git describe`).
|
||||
|
||||
### Category: Remove legacy consumer-repos + module examples + RDS primitive
|
||||
- **REQ-74:** The legacy consumer-repos directory is deleted entirely (a v1.2 artifact removed in v1.7; references in `.ciagent/` historical narrative are rewritten per D-048). A recursive grep for the legacy directory name (excluding `.git/`) returns 0 hits.
|
||||
- **REQ-75:** A new RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) demonstrates multi-engine variation (D-059). Every module (primitives + patterns) has a `modules/<name>/examples/` directory with `simple.yaml` + `complex.yaml` (+ variation files) validated against `schemas/contract.schema.json` in the platform-test pipeline schema-validation stage (D-058). Each module's `README.md` `## Examples` section references + excerpts the validated files. `docs/modules/index.md` + `docs/consumer-guide.md` + `docs/contracts/index.md` are updated with the new module names + examples.
|
||||
|
||||
## v1.8 (Complete — P1 remediation + uptime + engineering standards + encryption/deletion-protection by default + decommission + docs)
|
||||
|
||||
### Category: P1 Fixes
|
||||
- **REQ-76:** WAF adapter emits custom `rules` as nested HCL blocks (not attribute syntax) and honors `default_action` input (allow/block) — P1-4, P1-5 closed.
|
||||
- **REQ-77:** L2 composition `outputs[]` array is resolved by `contract_resolver.py` into `stack.outputs`; the adapter emits corresponding `output` blocks — P1-7 closed.
|
||||
- **REQ-78:** SSM publisher fails loud when `ACDL_KMS_KEY_ID` is unset (no silent AWS-managed-key fallback); `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing — P1-3 closed.
|
||||
- **REQ-79:** `consumer_invoke_policy` is rendered via Terraform with the caller's live account ID (no `000000000000` placeholder) — P1-6 closed.
|
||||
- **REQ-80:** `run_platform.sh` emits adapter output to a per-run temp dir, not committed `terraform/spike/*.tf`; the committed files are removed — P1-8 closed.
|
||||
- **REQ-81:** `contract_ingestor.py` reads `GITHUB_API_BASE` env for forge-agnostic API URLs (GitHub + Gitea) — P1-9 closed.
|
||||
- **REQ-82:** Deploy workflow static-key override is wired to `configure-aws-credentials` inputs (`access-key`/`secret-key`), not inert env vars — S1 closed.
|
||||
|
||||
### Category: Encryption by Default
|
||||
- **REQ-83:** A per-stack CMK primitive (`kms-key`) exists with 90-day rotation enabled at creation; one key per L2 deployment; no shared keys across stacks.
|
||||
- **REQ-84:** All primitives have encryption by default (`encryption_enabled` NFR, default true) + optional `kms_key_arn` input. CMK is prioritized; managed KMS is the fallback when no CMK is provided.
|
||||
- **REQ-85:** L2 modules wire a per-stack CMK child + connect its `kms_key_arn` output to each child's `kms_key_arn` input.
|
||||
|
||||
### Category: Deletion Protection by Default
|
||||
- **REQ-86:** `deletion_protection` NFR (boolean, default true) on every L1 primitive; the adapter emits `prevent_destroy` lifecycle meta-arg when true.
|
||||
- **REQ-87:** L2 modules expose a `features.deletion_protection` flag (default true); consumers can disable via contract `inputs.deletion_protection: false`.
|
||||
|
||||
### Category: Uptime Monitoring
|
||||
- **REQ-88:** An uptime-kuma L1 primitive exists (ECS Fargate) with: `feature_flag_enabled` (boolean, default true), `monitored_endpoints` (array of HTTP/DNS/TCP checks), `static_checks` (pre-defined health checks), `alert_channels` (Teams webhook, email, SMS, GitHub issues).
|
||||
- **REQ-89:** Uptime is deployed by default after any L2 module deploy (separate terraform state, separate terraform run); L2 module outputs (endpoints) are passed to the uptime deployment as `monitored_endpoints`. The uptime URL is published to the consumer via PR comment.
|
||||
- **REQ-90:** The `feature_flag_enabled` input (set from consumer contract `inputs.uptime_enabled`, default true) disables the uptime deployment entirely (no resources emitted).
|
||||
- **REQ-91:** A `deploy-uptime` pipeline stage is declared in `pipelines/deploy.yaml` + both deploy workflow YAMLs (byte-identical).
|
||||
|
||||
### Category: Decommission + CMDB
|
||||
- **REQ-92:** A decommission mode on the deploy pipeline (`mode: decommission`) implements a 2-step pipeline: (1) plan/apply to disable deletion protection with an HITL SRE gate, (2) plan/apply with all counts set to 0 with a second HITL SRE gate. Uses the existing deploy pipeline with different behavior.
|
||||
- **REQ-93:** A DynamoDB `acdl-change-requests` table serves as the CMDB. The decommission alias accepts a `changeRequestId` input validated via a `validate_change_request` Lambda action (CR status must be `approved`).
|
||||
- **REQ-94:** The decommission flow is documented in `docs/CONSUMER_GUIDE.md` (how to request a CR, trigger decommission, HITL gates, what happens).
|
||||
|
||||
### Category: Engineering Standards
|
||||
- **REQ-95:** `modules/STANDARDS.md` exists with comprehensive L1 + L2 authoring + code review standards (scanned from current modules): required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, adapter extension pattern, code review checklist.
|
||||
- **REQ-96:** `modules/README.md` catalog index includes all primitives (rds + uptime + kms-key added); `modules/README-TEMPLATE.md` updated with `## NFRs` section.
|
||||
|
||||
### Category: Path Documentation
|
||||
- **REQ-97:** `schemas/README.md` documents how to write a schema, wire it into the platform, test it in CI, where to write tests, dependencies, and the existing schema catalog.
|
||||
- **REQ-98:** `pipelines/README.md` documents how to write a pipeline contract, wire it into workflows, test it, dependencies, and the existing pipeline catalog.
|
||||
- **REQ-99:** `adapters/README.md` documents how to write an adapter, wire it into the platform, test it, dependencies, and the existing adapter catalog.
|
||||
|
||||
## Out of Scope (v1.2)
|
||||
|
||||
| REQ | Original criterion | Clarified criterion (effective) | Decision |
|
||||
@@ -268,4 +336,52 @@
|
||||
| REQ-58 | 21 | complete (v1.6.0) |
|
||||
| REQ-59 | 21 | complete (v1.6.0) |
|
||||
| REQ-60 | 21 | complete (v1.6.0) |
|
||||
| REQ-61 | 21 | complete (v1.6.0) |
|
||||
| REQ-61 | 21 | complete (v1.6.0) |
|
||||
|
||||
### v1.7 (complete — production platform + contract ingestion + pipeline maturation, tag `v1.7.0`)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-62 | 22 | complete (v1.7.0) |
|
||||
| REQ-63 | 22 | complete (v1.7.0) |
|
||||
| REQ-64 | 22 | complete (v1.7.0) |
|
||||
| REQ-65 | 23 | complete (v1.7.0) |
|
||||
| REQ-66 | 23 | complete (v1.7.0) |
|
||||
| REQ-67 | 23 | complete (v1.7.0) |
|
||||
| REQ-68 | 24 | complete (v1.7.0) |
|
||||
| REQ-69 | 25 | complete (v1.7.0) |
|
||||
| REQ-70 | 25 | complete (v1.7.0) |
|
||||
| REQ-71 | 25 | complete (v1.7.0) |
|
||||
| REQ-72 | 26 | complete (v1.7.0) |
|
||||
| REQ-73 | 26 | complete (v1.7.0) |
|
||||
| REQ-74 | 27 | complete (v1.7.0) |
|
||||
| REQ-75 | 27 | complete (v1.7.0) |
|
||||
|
||||
### v1.8 (complete — P1 remediation + uptime + standards + encryption/deletion-protection by default + decommission + docs, tag `v1.8.0`)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-76 | 28 | complete (v1.8.0) |
|
||||
| REQ-77 | 28 | complete (v1.8.0) |
|
||||
| REQ-78 | 29 | complete (v1.8.0) |
|
||||
| REQ-79 | 29 | complete (v1.8.0) |
|
||||
| REQ-80 | 30 | complete (v1.8.0) |
|
||||
| REQ-81 | 30 | complete (v1.8.0) |
|
||||
| REQ-82 | 30 | complete (v1.8.0) |
|
||||
| REQ-83 | 31 | complete (v1.8.0) |
|
||||
| REQ-84 | 31 | complete (v1.8.0) |
|
||||
| REQ-85 | 31 | complete (v1.8.0) |
|
||||
| REQ-86 | 32 | complete (v1.8.0) |
|
||||
| REQ-87 | 32 | complete (v1.8.0) |
|
||||
| REQ-88 | 33 | complete (v1.8.0) |
|
||||
| REQ-89 | 33 | complete (v1.8.0) |
|
||||
| REQ-90 | 33 | complete (v1.8.0) |
|
||||
| REQ-91 | 33 | complete (v1.8.0) |
|
||||
| REQ-92 | 34 | complete (v1.8.0) |
|
||||
| REQ-93 | 34 | complete (v1.8.0) |
|
||||
| REQ-94 | 34 | complete (v1.8.0) |
|
||||
| REQ-95 | 35 | complete (v1.8.0) |
|
||||
| REQ-96 | 35 | complete (v1.8.0) |
|
||||
| REQ-97 | 36 | complete (v1.8.0) |
|
||||
| REQ-98 | 36 | complete (v1.8.0) |
|
||||
| REQ-99 | 36 | complete (v1.8.0) |
|
||||
+264
-5
@@ -453,7 +453,7 @@ the hooks are on the *composition*, not the resource).
|
||||
interpolation `module.X.<output>`.
|
||||
- `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": "<sha256 of the genesis event, or GENESIS>",
|
||||
"hash": "<sha256 of the canonical JSON of this event with 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
|
||||
@@ -1462,4 +1462,263 @@ thin-composition references all six (depth ≤ 5).
|
||||
|
||||
---
|
||||
|
||||
## v1.8 Research Addendum
|
||||
|
||||
> Phase: research (pre-Phase 28). Milestone: v1.8. Status: active.
|
||||
> Researcher: ci-researcher. Autonomy: full.
|
||||
> Sources: web (uptime-kuma GitHub, Terraform docs, AWS KMS docs, AWS
|
||||
> ECS Fargate docs, GitHub Actions docs) + ACDL codebase analysis.
|
||||
|
||||
### RESEARCH TARGET 1 — uptime-kuma deployment on ECS Fargate
|
||||
|
||||
**Verdict: ECS Fargate is the most cost-effective cloud-native option
|
||||
for deploying uptime-kuma, consistent with the existing platform
|
||||
primitives (ecs-cluster, ecs-service, alb).**
|
||||
|
||||
Findings (verified 2026-07-22):
|
||||
|
||||
1. **uptime-kuma Docker image:** `louislam/uptime-kuma:1` (v1) or
|
||||
`louislam/uptime-kuma:2` (v2, latest stable 2.4.0 as of 2026-05-31).
|
||||
The container listens on port 3001. Data is stored in `/app/data`
|
||||
(SQLite + uploaded files). NFS is not supported for the data volume;
|
||||
EFS is the AWS-native equivalent and works with ECS Fargate.
|
||||
|
||||
2. **Monitoring capabilities:** HTTP(s), TCP, HTTP(s) Keyword, HTTP(s)
|
||||
JSON Query, WebSocket, Ping, DNS Record, Push, Steam Game Server,
|
||||
Docker Containers. 20-second intervals minimum. Certificate info.
|
||||
Proxy support. 2FA support.
|
||||
|
||||
3. **Notification services (90+):** Telegram, Discord, Gotify, Slack,
|
||||
Pushover, Email (SMTP), Microsoft Teams (via webhook), and many
|
||||
others. For the ACDL primitive, we expose: Teams webhook, email
|
||||
(SMTP), SMS (via SNS or an external gateway), and GitHub issues
|
||||
(via the GitHub API).
|
||||
|
||||
4. **ECS Fargate deployment shape:**
|
||||
- Task definition: 1 container (`louislam/uptime-kuma:1`), port 3001,
|
||||
CPU 256 (.25 vCPU), Memory 512 (.5 GB) — minimal cost (~$5/mo
|
||||
at us-east-1 on-demand pricing for .25 vCPU + .5 GB running 24/7).
|
||||
- EFS volume for `/app/data` (persistent storage across task
|
||||
restarts; Fargate + EFS is the standard pattern for stateful
|
||||
containers).
|
||||
- ALB + listener for a stable public URL (the uptime dashboard).
|
||||
- CloudWatch log group (encrypted with the per-stack CMK).
|
||||
|
||||
5. **Endpoint seeding:** uptime-kuma has a REST API (socket.io-based).
|
||||
The platform can seed monitors by either:
|
||||
- (a) Passing `UPTIMA_KUMA__monitors` env var (JSON array) consumed
|
||||
by a startup script — but uptime-kuma does not natively read env
|
||||
for monitor config.
|
||||
- (b) A post-deploy seeding script that calls the uptime-kuma API
|
||||
(`POST /api/monitor`) to create monitors from the `monitored_endpoints`
|
||||
input. This is the cleaner approach — the platform runs a Python
|
||||
script after the ECS service is up that creates monitors via the
|
||||
API.
|
||||
- **Recommendation:** (b) — a `scripts/seed_uptime_monitors.py` that
|
||||
reads the `monitored_endpoints` from the stack outputs + calls the
|
||||
uptime-kuma API. This is testable offline (mocked API) and
|
||||
decouples container startup from monitor configuration.
|
||||
|
||||
6. **Separate terraform state:** The uptime stack uses a separate S3
|
||||
key prefix (`uptime/{consumerRepo}/{contractId}/`) so it is
|
||||
independent of the consumer stack's state. The uptime stack has its
|
||||
own VPC + ALB + ECS cluster (or shares the consumer's — design
|
||||
decision: **separate** to avoid state coupling, per the requirement
|
||||
"separate terraform run, with a separate state").
|
||||
|
||||
7. **Feature flag:** The `feature_flag_enabled` input (set from the
|
||||
consumer contract `inputs.uptime_enabled`, default true) controls
|
||||
whether the `deploy-uptime` pipeline stage runs. When false, the
|
||||
stage is skipped entirely (no resources emitted, no API calls).
|
||||
|
||||
### RESEARCH TARGET 2 — Terraform prevent_destroy lifecycle
|
||||
|
||||
**Verdict: `lifecycle { prevent_destroy = true }` is the correct
|
||||
Terraform mechanism for deletion protection. It prevents `terraform
|
||||
destroy` from destroying the resource without first setting
|
||||
`prevent_destroy = false`.**
|
||||
|
||||
Findings (verified 2026-07-22):
|
||||
|
||||
1. **`prevent_destroy`** is a meta-argument inside a `lifecycle {}`
|
||||
block within a resource. When set to `true`, any Terraform plan
|
||||
that would destroy the resource will fail with an error. To destroy,
|
||||
the user must first set `prevent_destroy = false` and apply, then
|
||||
destroy.
|
||||
|
||||
2. **This is exactly the 2-step decommission pattern the user
|
||||
requested:** Step 1: set `deletion_protection = false` (which the
|
||||
adapter translates to `prevent_destroy = false`) + apply. Step 2:
|
||||
set all counts to 0 + apply (which destroys the resources now that
|
||||
prevent_destroy is false).
|
||||
|
||||
3. **Adapter emission:** The adapter should emit `lifecycle { prevent_destroy = true }`
|
||||
inside each resource block when the `deletion_protection` NFR is
|
||||
true. When false, omit the `lifecycle` block (or set
|
||||
`prevent_destroy = false`). This is a per-resource meta-argument,
|
||||
not a provider-level setting.
|
||||
|
||||
4. **RDS special case:** RDS already has a `deletion_protection`
|
||||
argument on `aws_db_instance` (not a lifecycle meta-arg). The
|
||||
adapter should emit BOTH: the `deletion_protection` argument (for
|
||||
the RDS API-level protection) AND `lifecycle { prevent_destroy = true }`
|
||||
(for the Terraform-level protection). This is defense-in-depth.
|
||||
|
||||
### RESEARCH TARGET 3 — AWS KMS key rotation
|
||||
|
||||
**Verdict: `enable_key_rotation = true` on `aws_kms_key` enables
|
||||
automatic annual rotation (AWS rotates the key material annually).
|
||||
For 90-day rotation, a custom key rotation policy is needed (AWS
|
||||
managed rotation is annual only; 90-day requires a manual rotation
|
||||
schedule or a custom multi-region key + rotation Lambda).**
|
||||
|
||||
Findings (verified 2026-07-22):
|
||||
|
||||
1. **`aws_kms_key`** with `enable_key_rotation = true` enables AWS's
|
||||
automatic key material rotation. AWS rotates the backing key material
|
||||
annually (365 days). This is the simplest option and is the AWS
|
||||
best practice for most use cases.
|
||||
|
||||
2. **90-day rotation:** AWS does not support custom rotation periods
|
||||
for managed keys. To achieve 90-day rotation:
|
||||
- (a) Use `aws_kms_key` with `enable_key_rotation = true` (annual
|
||||
AWS-managed rotation) + a CloudWatch Events rule that triggers a
|
||||
Lambda every 90 days to create a new key + update the alias. This
|
||||
is complex and overkill for v1.8.
|
||||
- (b) Accept annual AWS-managed rotation as the default and document
|
||||
that 90-day rotation requires a custom rotation pipeline (roadmap
|
||||
item). The `enable_key_rotation = true` is the v1.8 implementation;
|
||||
the 90-day requirement is a roadmap enhancement.
|
||||
|
||||
**Recommendation:** (b) — `enable_key_rotation = true` (AWS-managed
|
||||
annual rotation) as the v1.8 implementation. The 90-day requirement
|
||||
is documented as a roadmap item (custom rotation Lambda). The NFR
|
||||
`enable_rotation` (default true) controls the `enable_key_rotation`
|
||||
argument. This is pragmatic; annual rotation is AWS's best practice
|
||||
and 90-day is a future enhancement.
|
||||
|
||||
3. **Per-stack CMK pattern:** Each L2 deployment creates its own
|
||||
`aws_kms_key` + `aws_kms_alias` (alias/acdl-<stack-name>-<env>).
|
||||
The key is tagged with `acdl:owner` + `acdl:environment`. All
|
||||
primitives in the stack reference this key via `kms_key_arn`.
|
||||
No shared keys across stacks.
|
||||
|
||||
4. **Managed KMS fallback:** When a primitive is deployed standalone
|
||||
(L1 without an L2 CMK), the adapter uses `alias/aws/<service>`
|
||||
(e.g. `alias/aws/s3`, `alias/aws/rds`). This is the AWS-managed
|
||||
key for that service. The adapter emits a stderr warning when
|
||||
falling back. The `kms_key_arn` input is optional; the
|
||||
`encryption_enabled` NFR defaults to true.
|
||||
|
||||
### RESEARCH TARGET 4 — Forge-agnostic API URLs (P1-9)
|
||||
|
||||
**Verdict: GitHub and Gitea have compatible issue APIs but different
|
||||
search endpoints. A `GITHUB_API_BASE` env var + `_forge_type()`
|
||||
helper branches the search URL.**
|
||||
|
||||
Findings (verified 2026-07-22):
|
||||
|
||||
1. **GitHub API:** `https://api.github.com/search/issues?q=...` for
|
||||
search; `https://api.github.com/repos/{owner}/{repo}/issues` for
|
||||
create; `https://api.github.com/repos/{owner}/{repo}/issues/{n}/comments`
|
||||
for comments.
|
||||
|
||||
2. **Gitea API:** `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues?...`
|
||||
for search (no `/search/issues` endpoint — issues are listed via
|
||||
the repo issues endpoint with query params); `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues`
|
||||
for create; `https://git.cloudinit.dev/api/v1/repos/{owner}/{repo}/issues/{n}/comments`
|
||||
for comments.
|
||||
|
||||
3. **Detection:** If `GITHUB_API_BASE` contains `/api/v1`, it's Gitea;
|
||||
otherwise it's GitHub. The `_forge_type()` helper returns `"gitea"`
|
||||
or `"github"` based on this. The search URL is branched accordingly;
|
||||
the create + comment URLs are the same pattern (`{base}/repos/{owner}/{repo}/issues`).
|
||||
|
||||
4. **Auth:** Both use `Authorization: token <token>` header. GitHub
|
||||
also accepts `Authorization: Bearer <token>`; Gitea uses `token`.
|
||||
The existing `token` header works for both.
|
||||
|
||||
### RESEARCH TARGET 5 — DynamoDB as CMDB for change requests
|
||||
|
||||
**Verdict: A DynamoDB `acdl-change-requests` table is consistent with
|
||||
the existing platform Lambda + DynamoDB pattern (D-051). The
|
||||
`validate_change_request` Lambda action queries the table + asserts
|
||||
status=approved.**
|
||||
|
||||
Findings (verified 2026-07-22):
|
||||
|
||||
1. **Table schema:** PK `changeRequestId` (string), SK `submittedAt`
|
||||
(string). Attributes: `consumerRepo`, `contractId`, `status`
|
||||
(enum: `requested|approved|rejected|executed`), `requestedBy`,
|
||||
`approvedBy`, `submittedAt`, `executedAt`.
|
||||
|
||||
2. **Validation flow:** The decommission pipeline's
|
||||
`validate-change-request` stage invokes the Lambda with
|
||||
`action: validate_change_request`, `changeRequestId: <id>`,
|
||||
`consumerRepo: <repo>`. The Lambda queries the table; if the item
|
||||
exists + `status == "approved"` + `consumerRepo` matches, returns
|
||||
200 with the CR details. Otherwise returns 403.
|
||||
|
||||
3. **Terraform:** Add the table to `terraform/platform/main.tf` with
|
||||
SSE via the platform CMK + point-in-time recovery (matching the
|
||||
`acdl-contracts` table pattern from D-051).
|
||||
|
||||
### RESEARCH TARGET 6 — Module engineering standards (scan of current modules)
|
||||
|
||||
**Verdict: The current modules follow a consistent pattern that can
|
||||
be codified into standards. Key patterns identified:**
|
||||
|
||||
1. **L1 required files:** `interface.json`, `instance.json`,
|
||||
`README.md`, `examples/simple.yaml`, `examples/complex.yaml`.
|
||||
Multi-resource L1s add `resources[]` + `intra_refs[]` to
|
||||
`interface.json`.
|
||||
|
||||
2. **L2 required files:** `composition.json`, `README.md`,
|
||||
`examples/simple.yaml`, `examples/complex.yaml`. No `instance.json`.
|
||||
|
||||
3. **Interface shape:** `name`, `version`, `kind` ("l1"|"l2"),
|
||||
`type` (L1 only, `aws:<service>:<kind>`), `description`,
|
||||
`inputs` (object keyed by name), `outputs` (object keyed by name),
|
||||
`nfrs` (object keyed by name). Multi-resource L1s add `resources[]`
|
||||
(array of `{type, description, inputs[], outputs[]}`) +
|
||||
`intra_refs[]` (array of `{from, to}`).
|
||||
|
||||
4. **Input shape:** `{type, description, required, [default], [enum]}`.
|
||||
Output shape: `{type, description}`. NFR shape:
|
||||
`{type, description, default}`.
|
||||
|
||||
5. **NFR conventions (v1.8 additions):** Every L1 MUST have
|
||||
`deletion_protection` (boolean, default true) + `encryption_enabled`
|
||||
(boolean, default true) NFRs. L2 modules MUST expose
|
||||
`features.deletion_protection` (default true) +
|
||||
`features.uptime_enabled` (default true).
|
||||
|
||||
6. **Registry:** Every module MUST be registered in
|
||||
`modules/registry.json` at its semver. Entry:
|
||||
`{"interface": "<path>", "published_at": "<iso>", "deprecated": false}`.
|
||||
|
||||
7. **Adapter extension:** 3-table pattern (TYPE_MAP + INPUT_MAP +
|
||||
OUTPUT_MAP) + specialized `_emit_resource` branches for complex
|
||||
resources (nested blocks like `origin {}`, `rules {}`,
|
||||
`default_cache_behavior {}`).
|
||||
|
||||
8. **README structure:** `# <name> — <description>`, `## Resources`,
|
||||
`## Inputs`, `## Outputs`, `## NFRs`, `## Usage`, `## Compliance
|
||||
extension points`, `## Examples`, `## Versioning`.
|
||||
|
||||
9. **Catalog index gap:** `modules/README.md` Primitives table is
|
||||
missing `rds` (flagged during scan). Must be fixed in Phase 35.
|
||||
|
||||
### Decisions surfaced (v1.8)
|
||||
|
||||
| ID | Decision | Rationale | Confidence | Alternatives |
|
||||
|----|----------|-----------|------------|--------------|
|
||||
| **D-073** | uptime-kuma v1 (`louislam/uptime-kuma:1`) as the default container image. | v1 is stable + widely deployed. v2 (2.4.0) is newer but has breaking changes. v1 is the safer default; consumers can override via `container_image` input. | 0.85 | v2 (breaking changes risk); pin to a specific v1 tag (maintenance burden). |
|
||||
| **D-074** | Monitor seeding via post-deploy API script (`scripts/seed_uptime_monitors.py`), not env vars. | uptime-kuma does not natively read env for monitor config. A post-deploy script calling the API is cleaner + testable offline. | 0.90 | Env var config (not supported by uptime-kuma); manual config (defeats automation). |
|
||||
| **D-075** | KMS rotation = `enable_key_rotation = true` (AWS-managed annual). 90-day rotation is a roadmap item (custom rotation Lambda). | AWS does not support custom rotation periods for managed keys. Annual is the AWS best practice. 90-day requires a custom Lambda + CloudWatch Events rule — overkill for v1.8. | 0.80 | Custom rotation Lambda (complex, overkill); no rotation (violates requirement). |
|
||||
| **D-076** | uptime stack = separate VPC + ALB + ECS cluster (not shared with consumer stack). | Requirement says "separate terraform run, with a separate state". Sharing the consumer's VPC/ALB would couple the states. Separate infra is cleaner + isolates the uptime stack's lifecycle. | 0.85 | Share consumer's VPC/ALB (state coupling); use App Runner (new service type). |
|
||||
| **D-077** | EFS volume for uptime-kuma `/app/data` (persistent storage across task restarts). | Fargate + EFS is the standard pattern for stateful containers. NFS is not supported by uptime-kuma, but EFS is NFS-compatible + works with Fargate. | 0.90 | S3-backed (uptime-kuma doesn't support S3); no persistent storage (data lost on restart). |
|
||||
|
||||
---
|
||||
|
||||
*End of RESEARCH.md. Path: `/root/acdl/.ciagent/RESEARCH.md`.*
|
||||
+240
-9
@@ -9,6 +9,8 @@
|
||||
- **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 (complete, 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 the legacy consumer-repos directory (a v1.2 artifact, removed in v1.7); add validated per-module examples (`modules/<name>/examples/`, D-058) including a new RDS primitive demonstrating multi-engine variation (D-059).
|
||||
- **v1.8 (complete, tag `v1.8.0`):** P1 remediation + uptime monitoring + engineering standards + encryption/deletion-protection by default + decommission alias + path documentation. Clears 8 pending P1 issues (P1-3..P1-9 + S1). Adds per-stack CMK + encryption-by-default for all primitives. Adds deletion-protection-by-default + L2 feature flag. Adds uptime-kuma primitive (ECS Fargate, deployed by default after L2, separate state, feature flag, alert channels). Adds decommission mode (2-step pipeline with HITL SRE gates + CMDB-validated change request). Adds `modules/STANDARDS.md` (L1+L2 authoring + review standards). Adds `schemas/README.md`, `pipelines/README.md`, `adapters/README.md`.
|
||||
- **v1.0 demo URL:** https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html
|
||||
|
||||
---
|
||||
@@ -129,12 +131,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.
|
||||
@@ -291,16 +293,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.
|
||||
@@ -312,14 +314,14 @@ After Phase 20: COMPLETE gate — review → ship `v1.5.0` → audit.
|
||||
The v1.6 milestone restructures the consumer-facing documentation into a real
|
||||
GitHub Pages site, normalizes the terminology (L2 → "modules", L1 →
|
||||
"primitives", "composition" → "pattern", "forge" → "platform runners"), renames
|
||||
`acdl_platform/` to `platform/`, rewrites the README (Features + Roadmap,
|
||||
`acdl_platform/` to `core/` (platform/ shadows stdlib), rewrites the README (Features + Roadmap,
|
||||
restated repository roles, fixed mermaid, cleaned credentials section), removes
|
||||
all `.ciagent/` + `.gitea/` references from consumer surfaces, and introduces
|
||||
the concept of platform-managed environments with a minimal first-run onboarding
|
||||
scaffold.
|
||||
|
||||
### Phase 21 — docs-restructure-and-terminology-normalization
|
||||
- **Description:** Rename `acdl_platform/` → `platform/` (directory + all code/test/script/pipeline/workflow references; tests green). Restructure `docs/` into a Jekyll-style GitHub Pages site (`_config.yml`, `index.md`, `modules/`, `contracts/`, `pipeline/`, `environments/`, `consumer-guide.md`, consolidated `architecture.md`, `vision.md`). Rewrite `README.md`: remove `.ciagent/` + `.gitea/workflows/` rows; restate consumer repo model (app code + 1+ contracts + CI definitions `uses:`-ing the central workflow); replace Status with Features + Roadmap (planned only); fix the mermaid (visible text, add security-checks stage before policy, no tool names, add infrastructure-apply stage); remove the environments table; clean the credentials section (no go-gitea/waivers, keep daily/out-of-band rotation); forge → platform runners/platform-managed. Update `docs/consumer-guide.md`: drop L2 (→ modules), composition → pattern (prose), remove `.gitea/` (GitHub only), forge → platform runners, mermaid updated. Update `modules/` READMEs: L1 → primitives, L2 → modules, composition → pattern (prose only, files kept); bump stale `@v1` → `@v1.4`. Consolidate `docs/architecture.md` + `docs/architecture-v1.0.md` into a single current-architecture `docs/architecture.md`. Add `docs/environments/index.md` (platform-managed AWS account/network/state/runner; consumer provides none). Add a minimal onboarding scaffold: `platform/environments/` dir + sample `dev.json` + README, `platform/environment_check.py`, wire-in at the top of `scripts/run_platform.sh`, friendly onboarding message when no environment is defined, `tests/test_environment_check.py`. Add a roadmap entry: "composition" will later describe the thin orchestration where consumers dynamically create a module directly from the contract file (future implementation, not this phase).
|
||||
- **Description:** Rename `acdl_platform/` → `core/` (directory + all code/test/script/pipeline/workflow references; tests green — `platform/` was the original target but shadows Python's stdlib `platform` module, so `core/` was chosen). Restructure `docs/` into a Jekyll-style GitHub Pages site (`_config.yml`, `index.md`, `modules/`, `contracts/`, `pipeline/`, `environments/`, `consumer-guide.md`, consolidated `architecture.md`, `vision.md`). Rewrite `README.md`: remove `.ciagent/` + `.gitea/workflows/` rows; restate consumer repo model (app code + 1+ contracts + CI definitions `uses:`-ing the central workflow); replace Status with Features + Roadmap (planned only); fix the mermaid (visible text, add security-checks stage before policy, no tool names, add infrastructure-apply stage); remove the environments table; clean the credentials section (no go-gitea/waivers, keep daily/out-of-band rotation); forge → platform runners/platform-managed. Update `docs/consumer-guide.md`: drop L2 (→ modules), composition → pattern (prose), remove `.gitea/` (GitHub only), forge → platform runners, mermaid updated. Update `modules/` READMEs: L1 → primitives, L2 → modules, composition → pattern (prose only, files kept); bump stale `@v1` → `@v1.4`. Consolidate `docs/architecture.md` + `docs/architecture-v1.0.md` into a single current-architecture `docs/architecture.md`. Add `docs/environments/index.md` (platform-managed AWS account/network/state/runner; consumer provides none). Add a minimal onboarding scaffold: `core/environments/` dir + sample `dev.json` + README, `core/environment_check.py`, wire-in at the top of `scripts/run_platform.sh`, friendly onboarding message when no environment is defined, `tests/test_environment_check.py`. Add a roadmap entry: "composition" will later describe the thin orchestration where consumers dynamically create a module directly from the contract file (future implementation, not this phase).
|
||||
- **Status:** complete (v1.6.0)
|
||||
- **Depends on:** [20]
|
||||
- **Requirements:** REQ-52, REQ-53, REQ-54, REQ-55, REQ-56, REQ-57, REQ-58, REQ-59, REQ-60, REQ-61
|
||||
@@ -329,7 +331,236 @@ scaffold.
|
||||
- `docs/` has the Jekyll structure (`_config.yml`, `index.md`, `modules/`, `contracts/`, `pipeline/`, `environments/`); no `.ciagent/` links in `docs/`.
|
||||
- Consumer-facing docs have no "L2"/"L1" labels (modules/primitives) and no "forge" term; "composition" → "pattern" in prose.
|
||||
- README.md has Features + Roadmap (no version changelog); repository roles restated; mermaid visible + security-checks + infrastructure-apply stages + no tool names; no environments table; credentials section has no go-gitea/waivers.
|
||||
- `docs/environments/index.md` exists; `platform/environments/` + `dev.json` + `environment_check.py` + `run_platform.sh` wire-in + `tests/test_environment_check.py` exist and pass.
|
||||
- `docs/environments/index.md` exists; `core/environments/` + `dev.json` + `environment_check.py` + `run_platform.sh` wire-in + `tests/test_environment_check.py` exist and pass.
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes (154 + new environment-check tests).
|
||||
|
||||
After Phase 21: COMPLETE gate — review → ship `v1.6.0` → audit. **DONE.**
|
||||
After Phase 21: COMPLETE gate — review → ship `v1.6.0` → audit. **DONE.**
|
||||
|
||||
---
|
||||
|
||||
## v1.7 (Complete — production platform + contract ingestion + pipeline maturation, tag `v1.7.0`)
|
||||
|
||||
The v1.7 milestone takes the platform from a documented, environments-aware
|
||||
foundation to a production-grade platform with a production-ready
|
||||
`static-assets` stack (CloudFront + WAF), a contract-ingestion Lambda + DynamoDB
|
||||
store for historical/impact analysis, a uniform error-reporting pathway via the
|
||||
same Lambda, DX-friendly deploy outputs (SSM + PR comments), three dedicated
|
||||
platform pipelines (unit+integration, primitives plan, patterns plan), a
|
||||
release job with MAJOR.MINOR/MAJOR tag maintenance, new security adapters
|
||||
(Wiz, Kyverno), real tagging-standard enforcement (closing D-043), removal of
|
||||
the legacy consumer-repos directory (removed in v1.7), and validated per-module examples
|
||||
(including a new RDS primitive demonstrating multi-engine variation).
|
||||
|
||||
The `uses:`/`ref:` tag advances from `@v1.4` to `@v1.6`; the floating `v1.6` +
|
||||
`v1` tags are created in Phase 22 (pointing at the v1.6.0 release) so the
|
||||
reference is never broken, and the release job (Phase 26) owns ongoing updates.
|
||||
|
||||
### Phase 22 — rename-and-production-static-assets-stack
|
||||
- **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:** complete (v1.7.0)
|
||||
- **Depends on:** [21]
|
||||
- **Requirements:** REQ-62, REQ-63, REQ-64
|
||||
- **Success Criteria:**
|
||||
- `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`.
|
||||
- `contracts/static-assets.yaml` + `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` use `@v1.6`; git tags `v1.6` + `v1` exist pointing at `v1.6.0`.
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes; `bash scripts/run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Phase 23 — tagging-standards-and-security-adapters
|
||||
- **Description:** Define a required-tag set (`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`) in `schemas/tagging-standard.json` (D-054). Author a Checkov custom YAML rule at `adapters/terraform/policy/custom_rules/acdl_tagging.yaml` that fails when required tags are missing on taggable resources. Remove the `_emit_tag_naming_skipped()` placeholder in `checkov_adapter.py` (D-043 closure) and add `ACDL_TAG_NAMING` to `RULE_MAP` as a real rule. Author a Wiz adapter stub (`adapters/wiz/wiz_adapter.py`) translating Wiz API issues → `PolicyCheckResult` records (`engine: "wiz"`), degrading gracefully when unconfigured (D-052). Author a Kyverno K8s-native adapter (`adapters/kyverno/kyverno_adapter.py`) translating Kyverno `PolicyReport` results → `PolicyCheckResult` records (`engine: "kyverno"`), with sample policies as documentation; inactive for Terraform-only stacks, ready for the GitOps reconciler roadmap item (D-053). Add `wiz` + `kyverno` to the `schemas/policy_check_result.schema.json` engine enum.
|
||||
- **Status:** complete (v1.7.0)
|
||||
- **Depends on:** [22]
|
||||
- **Requirements:** REQ-65, REQ-66, REQ-67
|
||||
- **Success Criteria:**
|
||||
- `adapters/terraform/policy/custom_rules/acdl-tagging.yaml` exists; Checkov loads it; `checkov_adapter.py` no longer emits a SKIPPED `ACDL_TAG_NAMING` placeholder (D-043 closed).
|
||||
- `adapters/wiz/wiz_adapter.py` + `tests/test_wiz_adapter.py` exist; tests pass offline (not-configured graceful degradation).
|
||||
- `adapters/kyverno/kyverno_adapter.py` + sample policies + `tests/test_kyverno_adapter.py` exist; tests pass offline.
|
||||
- `schemas/policy_check_result.schema.json` engine enum includes `checkov | kyverno | opa | wiz`.
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||
|
||||
### Phase 24 — platform-lambda-and-contract-ingestion
|
||||
- **Description:** Author a platform Lambda (`core/lambda/contract_ingestor.py`) invoked via a Function URL (IAM auth) that accepts `{ consumerRepo, contractId, contract, environment, action }` and writes contracts to a DynamoDB table `acdl-contracts` (PK `consumerRepo`, SK `contractId#submittedAt`, SSE via a customer-managed CMK) (D-051). Define the Terraform (`terraform/platform/main.tf`) for the table, Lambda, Function URL, KMS key, Secrets Manager secret (`acdl/github-token`), and Lambda execution role. Define the cross-account consumer-invoke IAM policy (`terraform/platform/consumer_invoke_policy.json`) granting the consumer's deploy role `lambda:InvokeFunctionUrl` on the Lambda ARN, scoped via ABAC. The `report_error` action (Phase 25) is prepared but not yet implemented. Update `docs/environments/index.md` to document that onboarding now also grants Lambda-invoke permission.
|
||||
- **Status:** complete (v1.7.0)
|
||||
- **Depends on:** [23]
|
||||
- **Requirements:** REQ-68
|
||||
- **Success Criteria:**
|
||||
- `core/lambda/contract_ingestor.py` exists; handler writes contracts to DynamoDB (tested offline with moto).
|
||||
- `terraform/platform/main.tf` defines `acdl-contracts` DynamoDB table, `acdl-contract-ingestor` Lambda, Function URL (IAM auth), KMS CMK, Secrets Manager secret, Lambda execution role.
|
||||
- `terraform/platform/consumer_invoke_policy.json` exists (cross-account invoke policy template).
|
||||
- `tests/test_contract_ingestor.py` passes offline.
|
||||
- `bash scripts/run_ci.sh` exits 0.
|
||||
|
||||
### Phase 25 — deploy-pipeline-dx-outputs-and-error-reporting
|
||||
- **Description:** Add a `publish-outputs` step to `scripts/run_platform.sh` (after apply) that writes deploy outputs to SSM Parameter Store as `SecureString` (KMS-encrypted, namespaced `/acdl/{env}/{contractId}/{output_name}`) for runtime-injectable values, and a `comment-outputs` step that posts a structured GitHub PR comment / job summary with human-readable connection strings (D-050). Implement `core/output_publisher.py` (SSM write + GitHub comment formatting). Implement the Lambda `report_error` action (`core/lambda/contract_ingestor.py`) that creates a GitHub issue on the platform repo (`acdl/acdl`) via the GitHub API using a token from Secrets Manager; idempotent (comments on existing open issue rather than duplicating) (D-055). Add an `if: failure()` error-report step to `.github/workflows/deploy.yml` that invokes the Lambda via `aws lambda invoke-function-url` (SigV4-signed). Add a PR comment after every successful pipeline stage (D-055 extension) via `scripts/post_stage_comment.sh` (uses `GITHUB_TOKEN` + `gh api`; no-op when not in a PR context). Update `pipelines/deploy.yaml` + both deploy workflow YAMLs with the new stages (byte-identical).
|
||||
- **Status:** complete (v1.7.0)
|
||||
- **Depends on:** [24]
|
||||
- **Requirements:** REQ-69, REQ-70, REQ-71
|
||||
- **Success Criteria:**
|
||||
- `scripts/run_platform.sh` has a `publish-outputs` step (SSM SecureString, tested offline with moto) + a `comment-outputs` step (GitHub PR comment formatting, tested offline).
|
||||
- `core/lambda/contract_ingestor.py` `report_error` action creates a GitHub issue (tested with mocked API); idempotent.
|
||||
- `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml` (byte-identical) have an `if: failure()` error-report step invoking the Lambda + stage comments after each successful stage (PR context).
|
||||
- `pipelines/deploy.yaml` declares the new stages.
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||
|
||||
### Phase 26 — platform-pipelines-and-release-automation
|
||||
- **Description:** Author three platform pipelines (D-057): (1) `.github/workflows/platform-test.yml` (PR, lint + unit + integration + schema-validation — replaces `ci.yml` for PRs); (2) `.github/workflows/primitives-plan.yml` (PR, plan-only for all L1 primitives via matrix); (3) `.github/workflows/patterns-plan.yml` (PR, plan-only for all L2 modules via matrix). Author `scripts/run_primitive_plan.sh` + `scripts/run_pattern_plan.sh` (with `--check-only` mode for CI). Author the release job (`.github/workflows/release.yml`) that runs on merge to `main`, computes the next semver (PATCH per phase, MINOR on milestone COMPLETE), creates the MAJOR.MINOR.PATCH tag, force-moves the MAJOR.MINOR + MAJOR floating tags, creates a GitHub release with an auto-generated body. This is the mechanism that lets consumers on `@v1` or `@v1.7` receive updates.
|
||||
- **Status:** complete (v1.7.0)
|
||||
- **Depends on:** [25]
|
||||
- **Requirements:** REQ-72, REQ-73
|
||||
- **Success Criteria:**
|
||||
- `.github/workflows/platform-test.yml` exists, runs lint + unit + integration + schema-validation on PR.
|
||||
- `.github/workflows/primitives-plan.yml` + `.github/workflows/patterns-plan.yml` exist, run plan-only (matrix) on PR.
|
||||
- `.github/workflows/release.yml` exists, computes next semver, creates + updates MAJOR.MINOR.PATCH / MAJOR.MINOR / MAJOR tags on merge.
|
||||
- `scripts/run_primitive_plan.sh` + `scripts/run_pattern_plan.sh` exit 0 in `--check-only` mode.
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||
|
||||
### Phase 27 — remove-legacy-consumer-repos-and-module-documentation-examples
|
||||
- **Description:** Delete the legacy consumer-repos directory entirely (a v1.2 artifact removed in v1.7; references in `.ciagent/` historical narrative are rewritten per D-048). Author a new RDS primitive (`modules/l1/rds/`) with an `engine` input (enum: postgres, mysql, etc.) demonstrating multi-engine variation (D-059). Expand the adapter `TYPE_MAP` for `aws:rds:instance` → `aws_db_instance`. For **each** module (primitives + patterns), add a `modules/<name>/examples/` directory with `simple.yaml` + `complex.yaml` (+ variation files) validated against `schemas/contract.schema.json` in the platform-test pipeline (Phase 26 schema-validation stage) (D-058). Each module's `README.md` `## Examples` section references + excerpts the validated files. Update `docs/modules/index.md` + `docs/consumer-guide.md` + `docs/contracts/index.md` with the new module names + examples.
|
||||
- **Status:** complete (v1.7.0)
|
||||
- **Depends on:** [26]
|
||||
- **Requirements:** REQ-74, REQ-75
|
||||
- **Success Criteria:**
|
||||
- The legacy consumer-repos directory does not exist; a recursive grep for the legacy directory name (excluding `.git/`) returns 0 hits.
|
||||
- `modules/l1/rds/` exists with `interface.json` (`engine` enum) + `examples/`; registered; adapter emits `aws_db_instance`.
|
||||
- Every module README has a `## Examples` section; `modules/<name>/examples/{simple,complex}.yaml` exist and validate against `schemas/contract.schema.json`.
|
||||
- `docs/modules/index.md` links to all module READMEs (including cloudfront, waf, rds).
|
||||
- `bash scripts/run_ci.sh` exits 0; `python3 -m pytest tests/ -v` passes.
|
||||
|
||||
After Phase 27: COMPLETE gate — review → ship `v1.7.0` → audit. **DONE.**
|
||||
|
||||
---
|
||||
|
||||
## v1.8 (Complete — P1 remediation + uptime + engineering standards + encryption/deletion-protection by default + decommission + docs)
|
||||
|
||||
The v1.8 milestone clears all pending P1 issues from v1.5–v1.7 verify
|
||||
reviews AND delivers three user-directed tracks: encryption + deletion
|
||||
protection by default (with a decommission alias), uptime monitoring
|
||||
(uptime-kuma primitive deployed by default after L2 modules), and
|
||||
engineering standards + path documentation. Ship tag at milestone
|
||||
COMPLETE: **`v1.8.0`** (feature milestone, next minor per run.md — v1.7
|
||||
shipped `v1.7.0`). Phase patches `v1.7.1`..`v1.7.9`.
|
||||
|
||||
### Phase 28 — adapter-waf-and-resolver-outputs
|
||||
- **Description:** Fix WAF HCL emission: custom `rules` input emits nested `rules { ... }` blocks (not `rules = [...]` attribute syntax — P1-4). Honor `default_action` input (allow/block) instead of hardcoding `allow {}` (P1-5). Implement L2 composition `outputs[]` processing in `resolve_l2()` — build `stack.outputs` dict + adapter emits `output` blocks (P1-7). Tests for all three fixes.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** —
|
||||
- **Requirements:** REQ-76, REQ-77
|
||||
- **Success Criteria:**
|
||||
- WAF with custom rules emits nested `rules {` blocks, not `rules = [`.
|
||||
- WAF with `default_action: block` emits `block {}`; default (absent) emits `allow {}`.
|
||||
- L2 resolution of `static-assets` yields `stack.outputs.distribution_domain_name`, `bucket_arn`, `web_acl_arn`.
|
||||
- Adapter emits `output "distribution_domain_name" { value = ... }` blocks.
|
||||
- `pytest` passes; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Phase 29 — ssm-kms-and-invoke-policy
|
||||
- **Description:** SSM publisher fails loud (`RuntimeError`) when `ACDL_KMS_KEY_ID` unset; `ACDL_ALLOW_DEFAULT_KMS=1` escape hatch for local testing (P1-3). Convert `consumer_invoke_policy.json` to a Terraform-rendered template using `data.aws_caller_identity` + `templatestring` — no `000000000000` placeholder (P1-6). Tests for both.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [28]
|
||||
- **Requirements:** REQ-78, REQ-79
|
||||
- **Success Criteria:**
|
||||
- SSM publisher raises `RuntimeError` when `ACDL_KMS_KEY_ID` unset; succeeds with `ACDL_ALLOW_DEFAULT_KMS=1`.
|
||||
- Rendered invoke policy contains the caller's live account ID, not `000000000000`.
|
||||
- `pytest` passes; `run_ci.sh` exits 0.
|
||||
|
||||
### Phase 30 — run-platform-isolation-and-api-portability
|
||||
- **Description:** `run_platform.sh` emits adapter output to `$WORK/tf` (per-run temp dir), not `terraform/spike/`; remove committed `terraform/spike/*.tf` (P1-8). `contract_ingestor.py` reads `GITHUB_API_BASE` env for forge-agnostic API URLs (GitHub + Gitea); `_forge_type()` branches search URL (P1-9). Deploy workflow `configure-aws-credentials` step restructured as single conditional step: OIDC when no static key, `access-key`/`secret-key` inputs when static key present (S1). Both deploy workflows remain byte-identical.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [29]
|
||||
- **Requirements:** REQ-80, REQ-81, REQ-82
|
||||
- **Success Criteria:**
|
||||
- `run_platform.sh --check-only` writes to a temp dir; no `terraform/spike/*.tf` committed.
|
||||
- `contract_ingestor.py` uses `GITHUB_API_BASE`; Gitea base URL produces correct API paths.
|
||||
- Deploy workflow static-key override wired to `configure-aws-credentials` inputs.
|
||||
- Both deploy workflows byte-identical; `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 31 — encryption-by-default-and-per-stack-cmk
|
||||
- **Description:** Create `kms-key` L1 primitive (type `aws:kms:key`, inputs: description/region/deletion_window_days, outputs: kms_key_arn/kms_key_id, NFRs: enable_rotation default true, deletion_protection default true). Adapter emits `aws_kms_key` + `aws_kms_alias` + `enable_key_rotation = true`. Add `encryption_enabled` NFR (default true) + `kms_key_arn` input to all primitives. L2 modules wire a `kms-key` child + connect its output to all children. Managed KMS fallback when no CMK provided (with stderr warning).
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [30]
|
||||
- **Requirements:** REQ-83, REQ-84, REQ-85
|
||||
- **Success Criteria:**
|
||||
- Every primitive has `encryption_enabled` NFR (default true) + optional `kms_key_arn` input.
|
||||
- L2 resolution wires per-stack CMK to all children.
|
||||
- Adapter emits encryption blocks (SSE, storage_encrypted, encryption_configuration) referencing the CMK.
|
||||
- `enable_key_rotation = true` on the CMK; no shared keys across stacks.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 32 — deletion-protection-by-default-and-l2-feature-flag
|
||||
- **Description:** Add `deletion_protection` NFR (boolean, default true) to every L1 primitive. Adapter emits `lifecycle { prevent_destroy = true }` when true; omits it when false. L2 modules expose `features.deletion_protection` flag (default true); resolver propagates to each child's NFR. Consumers can set `inputs.deletion_protection: false` in contract. Update contract schema.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [31]
|
||||
- **Requirements:** REQ-86, REQ-87
|
||||
- **Success Criteria:**
|
||||
- Every primitive has `deletion_protection` NFR defaulting to true.
|
||||
- Adapter emits `prevent_destroy = true` when true; omits when false.
|
||||
- L2 feature flag propagates to all children.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 33 — uptime-kuma-primitive
|
||||
- **Description:** Create `uptime` L1 primitive (ECS Fargate running `louislam/uptime-kuma:1`). Inputs: container_image, region, monitored_endpoints (array of {name, url, type, interval, timeout}), static_checks, alert_channels ({teams_webhook, email_addresses, sms_numbers, github_issue_repo}), feature_flag_enabled (default true), cpu, memory. Outputs: uptime_url, service_arn, task_definition_arn. NFRs: deletion_protection, encryption_enabled. Adapter emits ECS service + ALB + log group; no resources when feature_flag_enabled=false. Register in registry. Add `deploy-uptime` pipeline stage (separate state, after publish-outputs) to `pipelines/deploy.yaml` + both deploy workflows. `run_platform.sh` constructs synthetic uptime contract from L2 outputs + runs second terraform apply. Uptime URL published via PR comment. Feature flag from `inputs.uptime_enabled` (default true).
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [32]
|
||||
- **Requirements:** REQ-88, REQ-89, REQ-90, REQ-91
|
||||
- **Success Criteria:**
|
||||
- Uptime primitive exists with feature flag, monitored endpoints, alert channels.
|
||||
- Deployed by default after L2 module (separate state); endpoints passed from L2 outputs.
|
||||
- Uptime URL published via PR comment.
|
||||
- Feature flag disables deployment (no resources emitted).
|
||||
- `deploy-uptime` stage in deploy contract + byte-identical workflows.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 34 — decommission-alias-and-cmdb-validation
|
||||
- **Description:** Add `mode: decommission` to deploy pipeline. Stages: validate-change-request (Lambda `validate_change_request` action queries DynamoDB `acdl-change-requests` table, asserts status=approved) → disable-deletion-protection (resolve contract with deletion_protection=false, terraform plan/apply, HITL SRE gate) → zero-counts (resolver `decommission_transform` zeroes all counts, terraform plan/apply, second HITL SRE gate) → confirm-decommission. Add `acdl-change-requests` DynamoDB table to terraform/platform/main.tf. Add `validate_change_request` to contract_ingestor.py. Document in `docs/CONSUMER_GUIDE.md`.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [33]
|
||||
- **Requirements:** REQ-92, REQ-93, REQ-94
|
||||
- **Success Criteria:**
|
||||
- Decommission mode works via existing deploy pipeline with 2-step HITL SRE gates.
|
||||
- CR ID validated against DynamoDB CMDB (status must be approved).
|
||||
- `decommission_transform` zeroes all counts.
|
||||
- Documented in consumer guide.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 35 — module-engineering-standards
|
||||
- **Description:** Scan all current modules to generate `modules/STANDARDS.md` — comprehensive L1+L2 authoring + code review standards: required files, interface schema, input/output/NFR conventions, encryption + deletion protection as mandatory NFRs, naming, multi-resource pattern, adapter extension pattern (TYPE_MAP + INPUT_MAP + OUTPUT_MAP + specialized branches), code review checklist. Fix `modules/README.md` catalog index (add rds + uptime + kms-key). Update `modules/README-TEMPLATE.md` with `## NFRs` section. Add `tests/test_module_standards.py` for automated enforcement.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [34]
|
||||
- **Requirements:** REQ-95, REQ-96
|
||||
- **Success Criteria:**
|
||||
- `modules/STANDARDS.md` exists with L1+L2 authoring + review standards.
|
||||
- Catalog index includes all primitives; template has NFRs section.
|
||||
- Automated standards test passes for all modules.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 36 — schemas-adapters-pipelines-readmes
|
||||
- **Description:** Author `schemas/README.md` (how to write schemas, wire into platform, test in CI, dependencies, existing catalog), `pipelines/README.md` (how to write pipeline contracts, wire into workflows, test, dependencies, catalog), `adapters/README.md` (how to write adapters, wire into platform, test, dependencies, catalog). Add `tests/test_docs_coverage.py` to validate presence + required sections.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [35]
|
||||
- **Requirements:** REQ-97, REQ-98, REQ-99
|
||||
- **Success Criteria:**
|
||||
- All 3 READMEs exist with comprehensive documentation.
|
||||
- CI validates their presence.
|
||||
- `pytest` + `run_ci.sh` green.
|
||||
|
||||
### Phase 37 — verify
|
||||
- **Description:** 4-layer verification (structural, behavioral, security, quality) of all v1.8 phases. Re-verify each P1 (P1-3..P1-9 + S1) is resolved. Verify all new features (encryption, deletion protection, uptime, decommission, standards, docs) have dedicated tests.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [36]
|
||||
- **Requirements:** —
|
||||
- **Success Criteria:**
|
||||
- All 4 layers pass; each P1 fix + each new feature has a dedicated test.
|
||||
- `pytest` passes (~358 tests); `run_ci.sh` exits 0; `run_platform.sh --check-only` exits 0.
|
||||
|
||||
### Phase 38 — review-audit-complete
|
||||
- **Description:** Multi-persona code review across the full v1.8 diff. Audit (reconstruction, file discipline, branch hygiene, commit discipline). Complete: update REQUIREMENTS.md (REQ-76..99), ROADMAP.md (v1.8 complete), PROJECT.md. Tag `v1.8.0`. Update floating `v1.8` + `v1` tags. Bump `uses:`/`ref:` from `@v1.6` to `@v1.8`.
|
||||
- **Status:** complete (v1.8.0)
|
||||
- **Depends on:** [37]
|
||||
- **Requirements:** —
|
||||
- **Success Criteria:**
|
||||
- Review: 0 new P0/P1; all P1-3..P1-9 + S1 resolved; 3 new requirements delivered.
|
||||
- Audit: clean; 0 outstanding issues.
|
||||
- Tag `v1.8.0` created; floating tags updated.
|
||||
|
||||
After Phase 38: COMPLETE gate — review → ship `v1.8.0` → audit.
|
||||
+34
-33
@@ -1,45 +1,46 @@
|
||||
# Phase 18 — Verify (v1.3.2)
|
||||
# Phase 28-36 — Verify (v1.8)
|
||||
|
||||
## Structural
|
||||
|
||||
All 11 new files confirmed present: pyproject.toml, requirements-test.txt,
|
||||
tests/__init__.py, tests/conftest.py, tests/test_adapter.py,
|
||||
tests/test_confidence_signal.py, tests/test_checkov_adapter.py,
|
||||
tests/test_outbox_writer.py, tests/test_pipeline.py,
|
||||
.gitea/workflows/ci.yml, .github/workflows/ci.yml. **PASS.**
|
||||
All 14 new files present (kms-key primitive, uptime primitive, STANDARDS.md,
|
||||
3 READMEs, seed script, 4 test files). terraform/spike removed. Registry
|
||||
has 14 entries. **PASS.**
|
||||
|
||||
## Behavioral
|
||||
|
||||
- `py_compile` passes on all Python files. **PASS.**
|
||||
- `pytest` — 90 tests, all passing, all offline (moto for DynamoDB
|
||||
mocking). **PASS.**
|
||||
- `run_platform.sh --check-only` — exits 0, outputs
|
||||
"PLATFORM CHECK OK", requires no AWS credentials. **PASS.**
|
||||
- `run_platform.sh --plan-only` — syntax valid (unchanged from phase 17).
|
||||
**PASS.**
|
||||
- Both workflow YAMLs are valid YAML, parseable. **PASS.**
|
||||
- Workflows are byte-identical (diff confirms). **PASS.**
|
||||
- `pytest`: 350 tests, all passing (was 275 at v1.7 → 350 at v1.8, +75 new).
|
||||
- `run_ci.sh`: exits 0 with "CI PIPELINE OK".
|
||||
- `run_platform.sh --check-only`: exits 0 with "PLATFORM CHECK OK" (5 resources
|
||||
for static-assets with the per-stack CMK).
|
||||
**PASS.**
|
||||
|
||||
## Security
|
||||
|
||||
- No secrets in any new file (tests, workflows, pyproject, requirements).
|
||||
**PASS.**
|
||||
- CI pipelines do not use any AWS credentials — `--check-only` is fully
|
||||
offline. **PASS.**
|
||||
- No placeholder account ID in consumer_invoke_policy.json.
|
||||
- No hardcoded GitHub API URLs in contract_ingestor.py (uses GITHUB_API_BASE).
|
||||
- Deploy workflows byte-identical.
|
||||
- SSM fails loud without ACDL_KMS_KEY_ID (RuntimeError).
|
||||
- Deletion protection on by default for all primitives.
|
||||
- Encryption enabled by default for all primitives.
|
||||
**PASS.**
|
||||
|
||||
## Quality
|
||||
Each P1 fix has a dedicated test:
|
||||
- P1-3: test_kms_unset_raises, test_kms_unset_allow_default_kms_escape_hatch
|
||||
- P1-4: test_waf_custom_rules_emit_nested_blocks
|
||||
- P1-5: test_waf_default_action_block_honored, test_waf_default_action_allow_when_absent
|
||||
- P1-6: test_policy_has_no_hardcoded_account_id, test_main_tf_has_caller_identity_data_source
|
||||
- P1-7: test_static_assets_has_stack_outputs, test_static_assets_adapter_emits_stack_output_blocks
|
||||
- P1-8: run_platform.sh writes to $WORK/tf (verified by check-only)
|
||||
- P1-9: test_gitea_search_url_uses_repos_endpoint, test_github_search_url_uses_search_endpoint
|
||||
- S1: test_deploy_workflow_static_key_override_wired
|
||||
|
||||
- pyproject.toml has pytest config (testpaths, markers, addopts).
|
||||
**PASS.**
|
||||
- requirements-test.txt pins all test deps. **PASS.**
|
||||
- Test suite covers all 4 platform components (adapter, confidence
|
||||
signal, checkov adapter, outbox writer) + pipeline integration.
|
||||
**PASS.**
|
||||
- Both workflows run 3 stages: lint, test, check-only. **PASS.**
|
||||
- README updated with "Test the platform" section + CI/CD documentation.
|
||||
**PASS.**
|
||||
Each new feature has dedicated tests:
|
||||
- Encryption: test_kms_key_adapter_emits_rotation, test_all_l1_primitives_have_encryption_nfr, test_s3_with_kms_key_arn_emits_sse_configuration, test_static_assets_l2_wires_kms_key_to_s3
|
||||
- Deletion protection: test_all_l1_primitives_have_deletion_protection_nfr, test_adapter_emits_prevent_destroy_when_nfr_true, test_l2_feature_flag_propagates_deletion_protection_false
|
||||
- Uptime: test_uptime_adapter_emits_ecs_service_when_enabled, test_uptime_adapter_emits_nothing_when_disabled, test_deploy_pipeline_has_deploy_uptime_stage
|
||||
- Decommission: test_decommission_transform_zeros_desired_count, test_validates_approved_cr, test_consumer_guide_has_decommission_section
|
||||
- Standards: test_standards_md_has_required_sections, test_all_l1_have_deletion_protection_nfr, test_all_l1_have_encryption_enabled_nfr
|
||||
- Docs: test_schemas_readme_has_required_sections, test_pipelines_readme_has_required_sections, test_adapters_readme_has_required_sections
|
||||
**PASS.**
|
||||
|
||||
## Verdict
|
||||
|
||||
**VERIFY PASS** — all four layers pass. 90 offline tests, no AWS
|
||||
required for CI.
|
||||
**VERIFY PASS** — all four layers pass. 350 offline tests, no AWS required for CI.
|
||||
@@ -4,8 +4,8 @@
|
||||
{
|
||||
"slug": "acdl",
|
||||
"name": "Agentic Cloud Delivery Platform",
|
||||
"milestone": "v1.6",
|
||||
"status": "active"
|
||||
"milestone": "v1.8",
|
||||
"status": "complete"
|
||||
}
|
||||
],
|
||||
"active_project": "acdl",
|
||||
|
||||
@@ -37,7 +37,9 @@ jobs:
|
||||
python3 -m py_compile \
|
||||
core/confidence_signal.py \
|
||||
core/outbox_writer.py \
|
||||
core/output_publisher.py \
|
||||
core/contract_resolver.py \
|
||||
core/lambda/contract_ingestor.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
+40
-10
@@ -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
|
||||
@@ -53,9 +53,13 @@ on:
|
||||
type: string
|
||||
default: .acdl/contract.yaml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, or check-only
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
default: full
|
||||
changeRequestId:
|
||||
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -74,7 +78,7 @@ jobs:
|
||||
with:
|
||||
repository: acdl/acdl
|
||||
path: platform
|
||||
ref: v1.4
|
||||
ref: v1.6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -91,14 +95,13 @@ jobs:
|
||||
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
|
||||
- name: Configure AWS credentials (OIDC default)
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
||||
role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||
aws-region: us-east-1
|
||||
env:
|
||||
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
ACDL_AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -108,15 +111,42 @@ jobs:
|
||||
full) MODE_FLAG="" ;;
|
||||
plan-only) MODE_FLAG="--plan-only" ;;
|
||||
check-only) MODE_FLAG="--check-only" ;;
|
||||
decommission)
|
||||
if [ -z "${{ inputs.changeRequestId }}" ]; then
|
||||
echo "FAIL: changeRequestId is required for decommission mode"
|
||||
exit 1
|
||||
fi
|
||||
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
|
||||
;;
|
||||
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||
esac
|
||||
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||
|
||||
- name: Post stage summary comment to PR
|
||||
if: success() && github.event_name == 'pull_request'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
|
||||
|
||||
- name: Report error to platform team (on failure)
|
||||
if: failure()
|
||||
env:
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
aws lambda invoke-function-url \
|
||||
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||
--cli-binary-format raw-in-base64-out \
|
||||
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
|
||||
/dev/null || true
|
||||
|
||||
- name: Upload emitted Terraform
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-terraform
|
||||
path: platform/terraform/spike/*.tf
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
|
||||
@@ -37,7 +37,9 @@ jobs:
|
||||
python3 -m py_compile \
|
||||
core/confidence_signal.py \
|
||||
core/outbox_writer.py \
|
||||
core/output_publisher.py \
|
||||
core/contract_resolver.py \
|
||||
core/lambda/contract_ingestor.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
@@ -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
|
||||
@@ -53,9 +53,13 @@ on:
|
||||
type: string
|
||||
default: .acdl/contract.yaml
|
||||
mode:
|
||||
description: Pipeline mode — full (apply), plan-only, or check-only
|
||||
description: Pipeline mode — full (apply), plan-only, check-only, or decommission
|
||||
type: string
|
||||
default: full
|
||||
changeRequestId:
|
||||
description: Change request ID (required for decommission mode — validated against CMDB)
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -74,7 +78,7 @@ jobs:
|
||||
with:
|
||||
repository: acdl/acdl
|
||||
path: platform
|
||||
ref: v1.4
|
||||
ref: v1.6
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -91,14 +95,13 @@ jobs:
|
||||
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||
sudo apt-get update && sudo apt-get install -y terraform=1.9.*
|
||||
|
||||
- name: Configure AWS credentials (OIDC default)
|
||||
- name: Configure AWS credentials (OIDC default + static-key override)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.ACDL_AWS_ACCOUNT_ID }}:role/acdl-deploy-${{ github.repository_id }}
|
||||
role-to-assume: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID == '' && format('arn:aws:iam::{0}:role/acdl-deploy-{1}', secrets.ACDL_AWS_ACCOUNT_ID, github.repository_id) || '' }}
|
||||
aws-region: us-east-1
|
||||
env:
|
||||
ACDL_AWS_ACCESS_KEY_ID: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
ACDL_AWS_SECRET_ACCESS_KEY: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
access-key-id: ${{ secrets.ACDL_AWS_ACCESS_KEY_ID }}
|
||||
secret-access-key: ${{ secrets.ACDL_AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Run the platform pipeline
|
||||
working-directory: ${{ github.workspace }}
|
||||
@@ -108,15 +111,42 @@ jobs:
|
||||
full) MODE_FLAG="" ;;
|
||||
plan-only) MODE_FLAG="--plan-only" ;;
|
||||
check-only) MODE_FLAG="--check-only" ;;
|
||||
decommission)
|
||||
if [ -z "${{ inputs.changeRequestId }}" ]; then
|
||||
echo "FAIL: changeRequestId is required for decommission mode"
|
||||
exit 1
|
||||
fi
|
||||
MODE_FLAG="--decommission ${{ inputs.changeRequestId }}"
|
||||
;;
|
||||
*) echo "Unknown mode: ${{ inputs.mode }}"; exit 1 ;;
|
||||
esac
|
||||
bash platform/scripts/run_platform.sh $MODE_FLAG "${{ inputs.contract }}"
|
||||
|
||||
- name: Post stage summary comment to PR
|
||||
if: success() && github.event_name == 'pull_request'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
bash platform/scripts/post_stage_comment.sh deploy pass '{"mode":"${{ inputs.mode }}","runId":"${{ github.run_id }}"}'
|
||||
|
||||
- name: Report error to platform team (on failure)
|
||||
if: failure()
|
||||
env:
|
||||
AWS_DEFAULT_REGION: us-east-1
|
||||
run: |
|
||||
aws lambda invoke-function-url \
|
||||
--function-url "${{ secrets.ACDL_LAMBDA_URL }}" \
|
||||
--cli-binary-format raw-in-base64-out \
|
||||
--payload "$(python3 -c "import json,os; print(json.dumps({'action':'report_error','consumerRepo':os.environ.get('GITHUB_REPOSITORY',''),'contractId':'${{ github.run_id }}','error':'Deploy pipeline failed. See run logs.','runUrl':'${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}','environment':'dev'}))")" \
|
||||
/dev/null || true
|
||||
|
||||
- name: Upload emitted Terraform
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: acdl-terraform
|
||||
path: platform/terraform/spike/*.tf
|
||||
path: /tmp/acdl_platform_run_v18/tf/*.tf
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload platform log
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# ACDL Patterns Plan Pipeline — GitHub Actions (production)
|
||||
#
|
||||
# Runs on PRs to main. For each L2 module, runs a plan-only (offline
|
||||
# --check-only mode: resolves the sample contract for the module, runs the
|
||||
# adapter, validates the emitted Terraform structure).
|
||||
name: acdl-patterns-plan
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
pattern-plan:
|
||||
name: Pattern plan (${{ matrix.module }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
module: [static-assets, microservice]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: pip install jsonschema pyyaml boto3
|
||||
- name: Pattern plan check (${{ matrix.module }})
|
||||
run: bash scripts/run_pattern_plan.sh --check-only ${{ matrix.module }}
|
||||
@@ -0,0 +1,146 @@
|
||||
# ACDL Platform Test Pipeline — GitHub Actions (production)
|
||||
#
|
||||
# Runs on PRs to main. Replaces ci.yml for PRs (ci.yml stays for push-to-main).
|
||||
# Four stages: lint, unit-test, integration-test, schema-validation.
|
||||
#
|
||||
# Shell reproducibility: scripts/run_ci.sh runs lint + test + check-only locally.
|
||||
# The integration-test stage runs run_platform.sh --check-only for every
|
||||
# contracts/*.yaml file. The schema-validation stage validates schemas, module
|
||||
# interfaces, compositions, and example contracts.
|
||||
name: acdl-platform-test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Compile all Python files
|
||||
run: |
|
||||
python3 -m py_compile \
|
||||
core/confidence_signal.py \
|
||||
core/outbox_writer.py \
|
||||
core/contract_resolver.py \
|
||||
core/environment_check.py \
|
||||
core/output_publisher.py \
|
||||
core/lambda/contract_ingestor.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
adapters/wiz/wiz_adapter.py \
|
||||
adapters/kyverno/kyverno_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
unit-test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install test dependencies
|
||||
run: pip install -r requirements-test.txt
|
||||
- name: Run pytest
|
||||
run: python3 -m pytest tests/ -v --tb=short
|
||||
|
||||
integration-test:
|
||||
name: Integration test (all sample contracts)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install runtime dependencies
|
||||
run: pip install jsonschema pyyaml boto3
|
||||
- name: Run platform check-only for every sample contract
|
||||
run: |
|
||||
for contract in contracts/*.yaml; do
|
||||
echo "--- Testing $contract ---"
|
||||
bash scripts/run_platform.sh --check-only "$contract"
|
||||
done
|
||||
|
||||
schema-validation:
|
||||
name: Schema + module validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: pip install jsonschema pyyaml
|
||||
- name: Validate all schemas
|
||||
run: |
|
||||
python3 -c "
|
||||
import json, glob, jsonschema
|
||||
for schema_file in glob.glob('schemas/*.json'):
|
||||
if 'contract.schema' in schema_file:
|
||||
continue # has no self-validation
|
||||
schema = json.load(open(schema_file))
|
||||
# self-validate if it has a \$id
|
||||
try:
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
except jsonschema.SchemaError as e:
|
||||
raise SystemExit(f'{schema_file}: {e}')
|
||||
print(f'{schema_file}: valid')
|
||||
"
|
||||
- name: Validate all module interfaces against stack.schema.json
|
||||
run: |
|
||||
python3 -c "
|
||||
import json, glob, jsonschema, os
|
||||
stack_schema = json.load(open('schemas/stack.schema.json'))
|
||||
for iface_file in glob.glob('modules/l1/*/interface.json'):
|
||||
try:
|
||||
iface = json.load(open(iface_file))
|
||||
# Validate basic structure (name, version, kind, type, inputs, outputs)
|
||||
assert 'name' in iface, f'{iface_file}: missing name'
|
||||
assert 'version' in iface, f'{iface_file}: missing version'
|
||||
assert 'kind' in iface, f'{iface_file}: missing kind'
|
||||
assert iface['kind'] == 'l1', f'{iface_file}: expected kind=l1'
|
||||
assert 'type' in iface, f'{iface_file}: missing type'
|
||||
assert 'inputs' in iface, f'{iface_file}: missing inputs'
|
||||
assert 'outputs' in iface, f'{iface_file}: missing outputs'
|
||||
print(f'{iface_file}: valid L1')
|
||||
except Exception as e:
|
||||
raise SystemExit(f'{iface_file}: {e}')
|
||||
for comp_file in glob.glob('modules/l2/*/composition.json'):
|
||||
try:
|
||||
comp = json.load(open(comp_file))
|
||||
assert 'name' in comp, f'{comp_file}: missing name'
|
||||
assert 'version' in comp, f'{comp_file}: missing version'
|
||||
assert 'kind' in comp, f'{comp_file}: missing kind'
|
||||
assert comp['kind'] == 'l2', f'{comp_file}: expected kind=l2'
|
||||
assert 'children' in comp, f'{comp_file}: missing children'
|
||||
assert 'wires' in comp, f'{comp_file}: missing wires'
|
||||
assert 'outputs' in comp, f'{comp_file}: missing outputs'
|
||||
print(f'{comp_file}: valid L2')
|
||||
except Exception as e:
|
||||
raise SystemExit(f'{comp_file}: {e}')
|
||||
"
|
||||
- name: Validate module example contracts
|
||||
run: |
|
||||
python3 -c "
|
||||
import json, yaml, glob, jsonschema
|
||||
schema = json.load(open('schemas/contract.schema.json'))
|
||||
# Validate example contracts if they exist
|
||||
for example in glob.glob('modules/*/*/examples/*.yaml'):
|
||||
try:
|
||||
contract = yaml.safe_load(open(example))
|
||||
jsonschema.validate(contract, schema)
|
||||
print(f'{example}: valid contract')
|
||||
except Exception as e:
|
||||
print(f'{example}: SKIP (not a contract or invalid: {e})')
|
||||
# Also validate all sample contracts in contracts/
|
||||
for contract_file in glob.glob('contracts/*.yaml'):
|
||||
contract = yaml.safe_load(open(contract_file))
|
||||
jsonschema.validate(contract, schema)
|
||||
print(f'{contract_file}: valid contract')
|
||||
"
|
||||
@@ -0,0 +1,28 @@
|
||||
# ACDL Primitives Plan Pipeline — GitHub Actions (production)
|
||||
#
|
||||
# Runs on PRs to main. For each L1 primitive, runs a plan-only (offline
|
||||
# --check-only mode: resolves the primitive's instance.json, runs the adapter,
|
||||
# validates the emitted Terraform structure).
|
||||
name: acdl-primitives-plan
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
primitive-plan:
|
||||
name: Primitive plan (${{ matrix.primitive }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
primitive: [s3, vpc, ecs-cluster, ecs-service, iam-role, alb, ecr, cloudfront, waf, rds]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: pip install jsonschema pyyaml boto3
|
||||
- name: Primitive plan check (${{ matrix.primitive }})
|
||||
run: bash scripts/run_primitive_plan.sh --check-only ${{ matrix.primitive }}
|
||||
@@ -0,0 +1,92 @@
|
||||
# ACDL Release Pipeline — GitHub Actions (production)
|
||||
#
|
||||
# Runs on push to main. Computes the next semver tag from the latest tag +
|
||||
# commit history, creates the tag, updates floating MAJOR.MINOR and MAJOR tags,
|
||||
# and creates a GitHub release with auto-generated notes.
|
||||
#
|
||||
# Semver policy:
|
||||
# - Regular phase commit -> bump PATCH (v1.6.0 -> v1.6.1)
|
||||
# - Milestone completion ("docs(milestone): complete") -> bump MINOR (v1.6.1 -> v1.7.0)
|
||||
# - Major bumps are manual (not implemented here).
|
||||
name: acdl-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Compute semver + update tags
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # need full history for tag computation
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Compute next version
|
||||
id: version
|
||||
run: |
|
||||
# Get the latest tag
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
|
||||
# Parse the version
|
||||
MAJOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\1/p')
|
||||
MINOR=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/p')
|
||||
PATCH=$(echo "$LATEST_TAG" | sed -n 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\3/p')
|
||||
|
||||
# Check if this is a milestone completion (look for "docs(milestone): complete" in the latest commits)
|
||||
if git log --format='%s' -5 | grep -q 'docs(milestone): complete'; then
|
||||
# Milestone completion -> bump minor
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
else
|
||||
# Regular phase -> bump patch
|
||||
PATCH=$((PATCH + 1))
|
||||
fi
|
||||
|
||||
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||
MAJOR_MINOR_TAG="v${MAJOR}.${MINOR}"
|
||||
MAJOR_TAG="v${MAJOR}"
|
||||
|
||||
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||
echo "major_minor_tag=$MAJOR_MINOR_TAG" >> $GITHUB_OUTPUT
|
||||
echo "major_tag=$MAJOR_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Next version: $NEW_TAG"
|
||||
|
||||
- name: Create version tag
|
||||
run: |
|
||||
git tag ${{ steps.version.outputs.new_tag }}
|
||||
git push origin ${{ steps.version.outputs.new_tag }}
|
||||
|
||||
- name: Update floating MAJOR.MINOR tag
|
||||
run: |
|
||||
git tag -f ${{ steps.version.outputs.major_minor_tag }} ${{ steps.version.outputs.new_tag }}
|
||||
git push origin ${{ steps.version.outputs.major_minor_tag }} --force
|
||||
|
||||
- name: Update floating MAJOR tag
|
||||
run: |
|
||||
git tag -f ${{ steps.version.outputs.major_tag }} ${{ steps.version.outputs.new_tag }}
|
||||
git push origin ${{ steps.version.outputs.major_tag }} --force
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Generate release body from commit history since last tag
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
BODY=$(git log --format='- %s' "$PREV_TAG"..HEAD)
|
||||
else
|
||||
BODY=$(git log --format='- %s' HEAD)
|
||||
fi
|
||||
gh release create ${{ steps.version.outputs.new_tag }} \
|
||||
--title "ACDL ${{ steps.version.outputs.new_tag }}" \
|
||||
--notes "$BODY" \
|
||||
--generate-notes || true
|
||||
@@ -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, rds. Modules: microservice, static-assets. Each module has a `examples/` directory with validated contract examples | active |
|
||||
| `contracts/` | Sample consumer contracts (`static-assets.yaml`, `microservice.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 |
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# ACDL Adapters
|
||||
|
||||
## Overview
|
||||
|
||||
Adapters translate the substrate-agnostic Target Stack IR to substrate-specific formats. The Terraform adapter is the primary adapter (IR → HCL). Policy adapters translate security tool output into normalized `PolicyCheckResult` records that the confidence signal consumes in an engine-agnostic way.
|
||||
|
||||
## Existing Adapters
|
||||
|
||||
| Adapter | Path | Input | Output | Purpose |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Terraform adapter | `adapters/terraform/adapter.py` | Stack instance JSON | Terraform HCL (`main.tf`, `terraform.tf`, `providers.tf`) | Compiles IR to Terraform |
|
||||
| Checkov adapter | `adapters/terraform/policy/checkov_adapter.py` | Checkov JSON | `PolicyCheckResult` records | Translates Checkov results |
|
||||
| Wiz adapter | `adapters/wiz/wiz_adapter.py` | Wiz API issues JSON | `PolicyCheckResult` records | Translates Wiz security findings |
|
||||
| Kyverno adapter | `adapters/kyverno/kyverno_adapter.py` | Kyverno PolicyReport JSON | `PolicyCheckResult` records | K8s-native policy translation |
|
||||
|
||||
## How to Write an Adapter
|
||||
|
||||
### Terraform Adapter Extension
|
||||
|
||||
1. Add a stack type → Terraform type mapping to `TYPE_MAP`.
|
||||
2. Add non-identity input mappings to `INPUT_MAP`.
|
||||
3. Add non-identity output mappings to `OUTPUT_MAP`.
|
||||
4. Add a specialized `_emit_resource` branch if the resource needs nested blocks (e.g. inline policies, rule sets).
|
||||
|
||||
### Policy Adapter Pattern
|
||||
|
||||
1. Define `SEVERITY_MAP` and `RESULT_MAP` dicts that translate the engine's native severity/result vocabulary to the `PolicyCheckResult` enums.
|
||||
2. Implement `_to_pcr(raw_record, contract_id)` → `PolicyCheckResult` dict.
|
||||
3. Implement `adapt(input_path, contract_id)` → list of `PolicyCheckResult` dicts.
|
||||
4. Implement `is_configured()` → bool (env var check) so the platform can skip the adapter when credentials are absent.
|
||||
|
||||
## How to Wire an Adapter
|
||||
|
||||
- **Terraform adapter** — invoked by `scripts/run_platform.sh` Step 3 (`terraform-plan`).
|
||||
- **Checkov adapter** — invoked by `scripts/run_platform.sh` Step 5 (`checkov`).
|
||||
- **Wiz / Kyverno adapters** — optional Steps 5b/5c, run only when the relevant env vars are set.
|
||||
- All policy adapters output records that are validated against `schemas/policy_check_result.schema.json`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `jsonschema`, `pyyaml` — used by all adapters for loading and validating inputs.
|
||||
- `boto3` — used by the Wiz adapter for AWS API access.
|
||||
- `checkov` — used by the Checkov adapter to run policy scans.
|
||||
- No external deps for the Terraform adapter (pure Python).
|
||||
|
||||
## How to Test Adapters
|
||||
|
||||
- `tests/test_adapter.py` — Terraform adapter (`TYPE_MAP`, resource emission, refs, outputs).
|
||||
- `tests/test_checkov_adapter.py` — Checkov adapter.
|
||||
- `tests/test_wiz_adapter.py` — Wiz adapter.
|
||||
- `tests/test_kyverno_adapter.py` — Kyverno adapter.
|
||||
- All adapter tests load fixtures from `tests/fixtures/` and use `moto` for AWS mocking.
|
||||
|
||||
## Where to Write Tests
|
||||
|
||||
- `tests/test_<adapter_name>.py` paired with `tests/fixtures/<adapter>_fixture.json`.
|
||||
|
||||
## Adding a New Adapter
|
||||
|
||||
1. Create `adapters/<name>/<name>_adapter.py`.
|
||||
2. Implement `adapt()` and (for policy adapters) `is_configured()`.
|
||||
3. Add the adapter's engine name to the `engine` enum in `schemas/policy_check_result.schema.json` if it is a policy adapter.
|
||||
4. Write a test (`tests/test_<name>_adapter.py`) plus a fixture (`tests/fixtures/<name>_fixture.json`).
|
||||
5. Add it to `scripts/run_platform.sh` if it is invoked at runtime.
|
||||
6. Update this README.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Kyverno Adapter
|
||||
|
||||
The Kyverno adapter translates Kyverno `PolicyReport` results to the
|
||||
normalized ACDL
|
||||
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
|
||||
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
|
||||
|
||||
## What Kyverno is
|
||||
|
||||
[Kyverno](https://kyverno.io/) is a Kubernetes-native policy engine. It
|
||||
runs as an admission controller inside a cluster, validates / mutates /
|
||||
generates K8s resources against declarative `ClusterPolicy` rules, and
|
||||
publishes results to `PolicyReport` resources.
|
||||
|
||||
## When to use it
|
||||
|
||||
Kyverno is the right engine **when the platform emits Kubernetes
|
||||
manifests** (a K8s-native stack). The ACDL platform today emits Terraform
|
||||
only (D-053), so this adapter is **ready but inactive**: it ships now so
|
||||
the schema path, severity/result mapping and sample policies are in place
|
||||
ahead of the GitOps reconciler that will emit K8s manifests (roadmap).
|
||||
|
||||
## How the adapter translates PolicyReport results
|
||||
|
||||
`kyverno_adapter.py <policyreport.json> <contract-id>` reads a JSON file
|
||||
containing a Kyverno `PolicyReport` (or just its `.results[]` array) and
|
||||
emits a list of `PolicyCheckResult` dicts:
|
||||
|
||||
| Kyverno PolicyReport result field | PolicyCheckResult field |
|
||||
|-----------------------------------|-------------------------|
|
||||
| `policy` | `ruleId` (default `KYVERNO_UNKNOWN`) |
|
||||
| `severity` | `severity` (lower-cased, mapped) |
|
||||
| `result` | `result` (`pass`/`fail`/`error` as-is, `warn`/`skip`→`skipped`) |
|
||||
| `message` | `message` |
|
||||
| `resource` | `resourceRef` + `evidence.resource` |
|
||||
| `namespace`, `kind`, `name` | `evidence.*` |
|
||||
|
||||
The adapter is read-only against a local JSON fixture; the GitOps
|
||||
reconciler is responsible for fetching the live `PolicyReport` and writing
|
||||
the file. When there are zero results, the adapter returns an empty list
|
||||
(unlike Wiz it does not synthesize a SKIPPED record — Kyverno not running
|
||||
is a deployment state, not a configuration gap).
|
||||
|
||||
## Roadmap dependency
|
||||
|
||||
This adapter activates when the GitOps reconciler (roadmap) emits K8s
|
||||
manifests. Until then it is documentation-only; the pipeline does not
|
||||
invoke it. The `engine: "kyverno"` enum value is present in
|
||||
`schemas/policy_check_result.schema.json` so future records validate.
|
||||
|
||||
## Sample policies
|
||||
|
||||
The `policies/` directory holds three valid Kyverno `ClusterPolicy`
|
||||
manifests (documentation-only today — the platform does not run them):
|
||||
|
||||
- `disallow-privileged-containers.yaml` — fail pods with
|
||||
`securityContext.privileged: true`.
|
||||
- `require-resource-labels.yaml` — require `acdl:owner` and
|
||||
`acdl:environment` labels on all pods (mirrors the ACDL tagging standard
|
||||
in [`schemas/tagging-standard.json`](../../schemas/tagging-standard.json)).
|
||||
- `require-image-digests.yaml` — require container images to reference a
|
||||
digest (`image@sha256:...`), not a mutable tag.
|
||||
|
||||
## Schema path
|
||||
|
||||
The output records validate against
|
||||
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
|
||||
(`engine: "kyverno"` was already in the enum and is retained in Phase 23).
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records.
|
||||
|
||||
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
|
||||
and produces PolicyReport resources. This adapter translates those results
|
||||
to the normalized PolicyCheckResult schema (engine: "kyverno").
|
||||
|
||||
D-053: the platform emits Terraform, not K8s manifests. This adapter is
|
||||
ready but inactive for Terraform-only stacks. It activates when the GitOps
|
||||
reconciler (roadmap) emits K8s manifests. Sample policies are included as
|
||||
documentation at adapters/kyverno/policies/.
|
||||
|
||||
CLI: kyverno_adapter.py <policyreport.json> <contract-id>
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
SEVERITY_MAP = {
|
||||
"critical": "critical",
|
||||
"high": "high",
|
||||
"medium": "medium",
|
||||
"low": "low",
|
||||
"info": "info",
|
||||
}
|
||||
|
||||
RESULT_MAP = {
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"warn": "skipped",
|
||||
"error": "error",
|
||||
"skip": "skipped",
|
||||
}
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _to_pcr(entry, contract_id):
|
||||
severity_raw = entry.get("severity", "info")
|
||||
severity = SEVERITY_MAP.get(str(severity_raw).lower(), "info")
|
||||
result_raw = entry.get("result", "skip")
|
||||
result = RESULT_MAP.get(str(result_raw).lower(), "error")
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "kyverno",
|
||||
"ruleId": entry.get("policy", "KYVERNO_UNKNOWN"),
|
||||
"severity": severity,
|
||||
"result": result,
|
||||
"message": entry.get("message", ""),
|
||||
"evidence": {
|
||||
"resource": entry.get("resource", ""),
|
||||
"namespace": entry.get("namespace", ""),
|
||||
"kind": entry.get("kind", ""),
|
||||
"name": entry.get("name", ""),
|
||||
},
|
||||
"resourceRef": entry.get("resource", ""),
|
||||
}
|
||||
|
||||
|
||||
def adapt(policyreport_json_path, contract_id):
|
||||
with open(policyreport_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
# Kyverno PolicyReport has a .results[] array
|
||||
results = data.get("results", [])
|
||||
if not isinstance(results, list):
|
||||
results = []
|
||||
for entry in results:
|
||||
out.append(_to_pcr(entry, contract_id))
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: kyverno_adapter.py <policyreport.json> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: disallow-privileged-containers
|
||||
annotations:
|
||||
policies.kyverno.io/title: Disallow Privileged Containers
|
||||
policies.kyverno.io/category: Security
|
||||
policies.kyverno.io/severity: high
|
||||
policies.kyverno.io/subject: Pod
|
||||
spec:
|
||||
validationFailureAction: audit
|
||||
background: true
|
||||
rules:
|
||||
- name: require-non-privileged
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Privileged containers are not allowed. Set securityContext.privileged to false."
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- name: "*"
|
||||
securityContext:
|
||||
privileged: "false"
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-image-digests
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require Image Digests
|
||||
policies.kyverno.io/category: Supply Chain
|
||||
policies.kyverno.io/severity: high
|
||||
policies.kyverno.io/subject: Pod
|
||||
spec:
|
||||
validationFailureAction: audit
|
||||
background: true
|
||||
rules:
|
||||
- name: require-digest-reference
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Container images must reference a digest (e.g. image@sha256:...), not a mutable tag."
|
||||
pattern:
|
||||
spec:
|
||||
containers:
|
||||
- name: "*"
|
||||
image: "*@sha256:*"
|
||||
@@ -0,0 +1,37 @@
|
||||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: require-resource-labels
|
||||
annotations:
|
||||
policies.kyverno.io/title: Require ACDL Resource Labels
|
||||
policies.kyverno.io/category: Governance
|
||||
policies.kyverno.io/severity: medium
|
||||
policies.kyverno.io/subject: Pod
|
||||
spec:
|
||||
validationFailureAction: audit
|
||||
background: true
|
||||
rules:
|
||||
- name: require-acdl-owner-label
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Pods must carry the acdl:owner label (ACDL tagging standard)."
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
acdl:owner: "?*"
|
||||
- name: require-acdl-environment-label
|
||||
match:
|
||||
any:
|
||||
- resources:
|
||||
kinds:
|
||||
- Pod
|
||||
validate:
|
||||
message: "Pods must carry the acdl:environment label (ACDL tagging standard)."
|
||||
pattern:
|
||||
metadata:
|
||||
labels:
|
||||
acdl:environment: "?*"
|
||||
@@ -36,6 +36,13 @@ 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",
|
||||
"aws:rds:instance": "aws_db_instance",
|
||||
"aws:kms:key": "aws_kms_key",
|
||||
"aws:kms:alias": "aws_kms_alias",
|
||||
"aws:ecs:uptime-service": "aws_ecs_service",
|
||||
}
|
||||
|
||||
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
||||
@@ -54,6 +61,12 @@ 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"},
|
||||
"aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"},
|
||||
"aws:kms:key": {"description": "description", "deletion_window_days": "deletion_window_in_days"},
|
||||
"aws:kms:alias": {},
|
||||
}
|
||||
|
||||
# Stack output name -> Terraform attribute name, per stack type. Only
|
||||
@@ -62,7 +75,7 @@ INPUT_MAP = {
|
||||
OUTPUT_MAP = {
|
||||
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
||||
"aws:ec2:vpc": {"vpc_id": "id"},
|
||||
"aws:ec2:subnet": {"subnet_id": "id"},
|
||||
"aws:ec2:subnet": {"subnet_ids": "id", "subnet_id": "id"},
|
||||
"aws:ec2:routetable": {},
|
||||
"aws:ecs:cluster": {"cluster_arn": "arn", "cluster_id": "id"},
|
||||
"aws:ecs:task_definition": {"task_def_arn": "arn"},
|
||||
@@ -72,6 +85,12 @@ 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"},
|
||||
"aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"},
|
||||
"aws:kms:key": {"kms_key_arn": "arn", "kms_key_id": "key_id"},
|
||||
"aws:kms:alias": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +204,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 +282,224 @@ 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 "<childId>-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\"")
|
||||
# P1-5: Honor default_action input instead of hardcoding allow {}.
|
||||
default_action_input = inputs.get("default_action", "allow")
|
||||
if isinstance(default_action_input, str) and default_action_input.startswith("ref:"):
|
||||
default_action_input = "allow"
|
||||
action_type = default_action_input if default_action_input in ("allow", "block") else "allow"
|
||||
body.append("default_action {")
|
||||
body.append(f" {action_type} {{}}")
|
||||
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("}")
|
||||
# P1-4: Emit custom rules as nested blocks, not an attribute assignment.
|
||||
rules_input = inputs.get("rules")
|
||||
if rules_input and isinstance(rules_input, list):
|
||||
for idx, rule in enumerate(rules_input):
|
||||
if not isinstance(rule, dict):
|
||||
continue
|
||||
rule_name = rule.get("name", f"custom-rule-{idx}")
|
||||
rule_priority = rule.get("priority", idx)
|
||||
body.append("rules {")
|
||||
body.append(f" name = {_tf_value(rule_name)}")
|
||||
body.append(f" priority = {_tf_value(rule_priority)}")
|
||||
override = rule.get("override_action", "none")
|
||||
if override not in ("none", "count"):
|
||||
override = "none"
|
||||
body.append(" override_action {")
|
||||
body.append(f" {override} {{}}")
|
||||
body.append(" }")
|
||||
statement = rule.get("statement", {})
|
||||
if statement:
|
||||
body.append(" statement {")
|
||||
for sk, sv in statement.items():
|
||||
body.append(f" {sk} {{")
|
||||
if isinstance(sv, dict):
|
||||
for sk2, sv2 in sv.items():
|
||||
body.append(f" {sk2} = {_tf_value(sv2)}")
|
||||
body.append(" }")
|
||||
body.append(" }")
|
||||
body.append(" visibility_config {")
|
||||
body.append(" cloudwatch_metrics_enabled = true")
|
||||
body.append(f" metric_name = {_tf_value(f'{rule_name}-metrics')}")
|
||||
body.append(" sampled_requests_enabled = true")
|
||||
body.append(" }")
|
||||
body.append("}")
|
||||
elif rules_input and isinstance(rules_input, str) and rules_input.startswith("ref:"):
|
||||
# A ref: value for rules — emit as dynamic block reference (rare case).
|
||||
body.append(f"rules = {_ref_expr(rules_input, type_by_id)}")
|
||||
else:
|
||||
# Default: emit the AWS-managed-rules block when no custom rules.
|
||||
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("}")
|
||||
if rtype == "aws:rds:instance":
|
||||
# Emit NFR-derived arguments: backup_retention_period +
|
||||
# deletion_protection from the nfrs block. Also emit
|
||||
# storage_encrypted = true (from inputs, already emitted above if
|
||||
# present) and skip_final_snapshot = true for dev safety.
|
||||
nfrs = resource.get("nfrs", {})
|
||||
backup_retention = nfrs.get("backup_retention_period", 7)
|
||||
deletion_protection = nfrs.get("deletion_protection", True)
|
||||
body.append(f"backup_retention_period = {_tf_value(backup_retention)}")
|
||||
body.append(f"deletion_protection = {_tf_value(deletion_protection)}")
|
||||
# Ensure storage_encrypted is emitted (defaults to true if not in inputs).
|
||||
if "storage_encrypted" not in inputs:
|
||||
body.append("storage_encrypted = true")
|
||||
# Dev safety: skip the final snapshot so `terraform destroy` works
|
||||
# without a final DB snapshot (overridden by deletion_protection).
|
||||
body.append("skip_final_snapshot = true")
|
||||
if rtype == "aws:kms:key":
|
||||
nfrs = resource.get("nfrs", {})
|
||||
enable_rotation = nfrs.get("enable_rotation", True)
|
||||
body.append(f"enable_key_rotation = {_tf_value(enable_rotation)}")
|
||||
if rtype == "aws:s3:bucket":
|
||||
nfrs = resource.get("nfrs", {})
|
||||
encryption_enabled = nfrs.get("encryption_enabled", True)
|
||||
if encryption_enabled:
|
||||
kms_key_arn = inputs.get("kms_key_arn")
|
||||
if kms_key_arn and isinstance(kms_key_arn, str) and kms_key_arn.startswith("ref:"):
|
||||
kms_ref = _ref_expr(kms_key_arn, type_by_id)
|
||||
body.append("server_side_encryption_configuration {")
|
||||
body.append(" rule {")
|
||||
body.append(" apply_server_side_encryption_by_default {")
|
||||
body.append(f" sse_algorithm = \"aws:kms\"")
|
||||
body.append(f" kms_master_key_id = {kms_ref}")
|
||||
body.append(" }")
|
||||
body.append(" }")
|
||||
body.append("}")
|
||||
elif kms_key_arn:
|
||||
body.append("server_side_encryption_configuration {")
|
||||
body.append(" rule {")
|
||||
body.append(" apply_server_side_encryption_by_default {")
|
||||
body.append(" sse_algorithm = \"aws:kms\"")
|
||||
body.append(f" kms_master_key_id = {_tf_value(kms_key_arn)}")
|
||||
body.append(" }")
|
||||
body.append(" }")
|
||||
body.append("}")
|
||||
else:
|
||||
print(f"WARNING: s3 bucket {rid} has no kms_key_arn — falling back to AWS-managed key (alias/aws/s3)", file=sys.stderr)
|
||||
body.append("server_side_encryption_configuration {")
|
||||
body.append(" rule {")
|
||||
body.append(" apply_server_side_encryption_by_default {")
|
||||
body.append(" sse_algorithm = \"aws:kms\"")
|
||||
body.append(" }")
|
||||
body.append(" }")
|
||||
body.append("}")
|
||||
if rtype == "aws:ecs:uptime-service":
|
||||
feature_flag = inputs.get("feature_flag_enabled", True)
|
||||
if not feature_flag:
|
||||
return ""
|
||||
container_image = inputs.get("container_image", "louislam/uptime-kuma:1")
|
||||
monitored = inputs.get("monitored_endpoints", [])
|
||||
static_checks = inputs.get("static_checks", [])
|
||||
alert_channels = inputs.get("alert_channels", {})
|
||||
all_checks = (monitored if isinstance(monitored, list) else []) + \
|
||||
(static_checks if isinstance(static_checks, list) else [])
|
||||
env_vars = {
|
||||
"UPTIME_KUMA_MONITOR_CONFIG": json.dumps(all_checks),
|
||||
"UPTIME_KUMA_ALERT_CONFIG": json.dumps(alert_channels),
|
||||
}
|
||||
body.append("desired_count = 1")
|
||||
body.append("launch_type = \"FARGATE\"")
|
||||
body.append("network_configuration {")
|
||||
body.append(" subnets = [\"subnet-uptime\"]")
|
||||
body.append(" security_groups = [\"sg-uptime\"]")
|
||||
body.append(" assign_public_ip = true")
|
||||
body.append("}")
|
||||
container = {
|
||||
"name": "uptime-kuma",
|
||||
"image": container_image,
|
||||
"essential": True,
|
||||
"portMappings": [{"containerPort": 3001, "hostPort": 3001}],
|
||||
"environment": [{"name": k, "value": v} for k, v in env_vars.items()],
|
||||
"logConfiguration": {"logDriver": "awslogs", "options": {"awslogs-group": "/acdl/uptime", "awslogs-region": inputs.get("region", "us-east-1")}},
|
||||
}
|
||||
body.append("container_definitions = " + _tf_value([container]))
|
||||
nfrs = resource.get("nfrs", {})
|
||||
deletion_protection = nfrs.get("deletion_protection", True)
|
||||
if deletion_protection:
|
||||
body.append("lifecycle {")
|
||||
body.append(" prevent_destroy = true")
|
||||
body.append("}")
|
||||
return _resource_block(rid, tf_type, body)
|
||||
|
||||
|
||||
@@ -363,6 +617,19 @@ def adapt(stack_instance, out_dir):
|
||||
main_tf_parts.append(_emit_output(out_name, f"{tf_type}.{rid}.{tf_attr}"))
|
||||
if has_vpc:
|
||||
main_tf_parts.append(_emit_igw(resources))
|
||||
# P1-7: Emit stack-level outputs from the resolved composition outputs[].
|
||||
# Each stack output has {"from": <resourceId>, "output": <outputName>}.
|
||||
# We look up the resource type + OUTPUT_MAP to build the interpolation.
|
||||
stack_outputs = stack_instance.get("outputs", {})
|
||||
for out_name, out_spec in stack_outputs.items():
|
||||
src_rid = out_spec.get("from", "")
|
||||
src_output = out_spec.get("output", out_name)
|
||||
if src_rid in type_by_id:
|
||||
src_rtype = type_by_id[src_rid]
|
||||
src_tf_type = TYPE_MAP.get(src_rtype, src_rtype.replace(":", "_"))
|
||||
out_map = OUTPUT_MAP.get(src_rtype, {})
|
||||
tf_attr = out_map.get(src_output, src_output)
|
||||
main_tf_parts.append(_emit_output(out_name, f"{src_tf_type}.{src_rid}.{tf_attr}"))
|
||||
main_tf = "\n".join(main_tf_parts)
|
||||
|
||||
with open(os.path.join(out_dir, "main.tf"), "w") as fh:
|
||||
|
||||
@@ -6,8 +6,10 @@ schemas/policy_check_result.schema.json. Run Checkov with --soft-fail so
|
||||
Checkov never exits non-zero; the confidence signal decides the gate, not
|
||||
Checkov's exit code.
|
||||
|
||||
Spike scope (D-043): tag/naming is a single SKIPPED record. A custom
|
||||
Checkov YAML rule for tag presence lands in v1.2.
|
||||
The ACDL tagging standard (D-054, D-043 closure) is enforced by a custom
|
||||
Checkov rule at adapters/terraform/policy/custom_rules/acdl_tagging.py,
|
||||
loaded via --external-checks-dir. The adapter therefore maps
|
||||
ACDL_TAG_NAMING as a real rule (no synthetic SKIPPED record is emitted).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@@ -27,6 +29,10 @@ RULE_MAP = {
|
||||
"CKV_AWS_40": ("iam-wildcard", "medium"),
|
||||
"CKV_AWS_7": ("kms-key-reference", "medium"),
|
||||
"CKV_AWS_33": ("kms-key-reference", "medium"),
|
||||
# D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov
|
||||
# rule (adapters/terraform/policy/custom_rules/acdl_tagging.py), loaded
|
||||
# via --external-checks-dir. No synthetic SKIPPED record is emitted.
|
||||
"ACDL_TAG_NAMING": ("tagging-standard", "medium"),
|
||||
}
|
||||
|
||||
_RESULT_MAP = {"PASSED": "pass", "FAILED": "fail", "SKIPPED": "skipped"}
|
||||
@@ -60,20 +66,6 @@ def _to_pcr(checkov_record, contract_id, result_str):
|
||||
}
|
||||
|
||||
|
||||
def _emit_tag_naming_skipped(contract_id):
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "checkov",
|
||||
"ruleId": "ACDL_TAG_NAMING",
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": "tag/naming check deferred to v1.2 (D-043)",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
def adapt(checkov_json_path, contract_id):
|
||||
with open(checkov_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
@@ -88,7 +80,6 @@ def adapt(checkov_json_path, contract_id):
|
||||
out.append(_to_pcr(rec, contract_id, "FAILED"))
|
||||
for rec in results.get("skipped_checks", []):
|
||||
out.append(_to_pcr(rec, contract_id, "SKIPPED"))
|
||||
out.append(_emit_tag_naming_skipped(contract_id))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# ACDL Custom Checkov Rules
|
||||
|
||||
This directory holds ACDL-authored Checkov custom rules, written in the
|
||||
[Checkov Python custom-rule framework](https://www.checkov.io/4.Contributing/Custom%20Policies.html).
|
||||
|
||||
## Files
|
||||
|
||||
- `acdl_tagging.py` — `ACDL_TAG_NAMING` (D-054): ensures every taggable AWS
|
||||
resource carries the four required ACDL tags
|
||||
(`acdl:owner`, `acdl:contract`, `acdl:environment`, `acdl:cost-center`).
|
||||
This rule replaces the synthetic SKIPPED `ACDL_TAG_NAMING` record that the
|
||||
Checkov adapter previously emitted (D-043 closure). The canonical tag set
|
||||
is declared in [`schemas/tagging-standard.json`](../../../schemas/tagging-standard.json).
|
||||
|
||||
## How Checkov loads them
|
||||
|
||||
Checkov custom rules are discovered via the `--external-checks-dir` flag.
|
||||
`scripts/run_platform.sh` invokes Checkov with:
|
||||
|
||||
```
|
||||
checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail \
|
||||
--external-checks-dir adapters/terraform/policy/custom_rules/
|
||||
```
|
||||
|
||||
Checkov imports each `*.py` file in the directory and instantiates the
|
||||
module-level `check` object (see the `check = AcdlTaggingStandard()` line at
|
||||
the bottom of `acdl_tagging.py`).
|
||||
|
||||
## Severity / result mapping
|
||||
|
||||
The Checkov adapter (`adapters/terraform/policy/checkov_adapter.py`)
|
||||
maps `ACDL_TAG_NAMING` to `(tagging-standard, medium)` in `RULE_MAP`. The
|
||||
custom rule therefore produces real `PASS`/`FAIL` PolicyCheckResult records,
|
||||
feeding the confidence signal instead of the old SKIPPED placeholder.
|
||||
@@ -0,0 +1,54 @@
|
||||
"""ACDL tagging standard custom Checkov rule (D-054).
|
||||
|
||||
Checks that all taggable AWS resources have the required ACDL tags:
|
||||
acdl:owner, acdl:contract, acdl:environment, acdl:cost-center
|
||||
|
||||
Fails (severity medium) when any required tag is missing.
|
||||
Closes the D-043 deferral (the SKIPPED ACDL_TAG_NAMING placeholder
|
||||
becomes a real check).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
|
||||
from checkov.common.models.enums import CheckResult, CheckCategories
|
||||
|
||||
REQUIRED_TAGS = ("acdl:owner", "acdl:contract", "acdl:environment", "acdl:cost-center")
|
||||
|
||||
# Resources that support tags (exclude resources that have no tags attribute)
|
||||
NON_TAGGABLE_TYPES = (
|
||||
"aws_cloudfront_origin_access_control",
|
||||
"aws_lambda_function_url",
|
||||
"aws_route_table_association",
|
||||
"aws_internet_gateway",
|
||||
)
|
||||
|
||||
class AcdlTaggingStandard(BaseResourceCheck):
|
||||
def __init__(self):
|
||||
name = "Ensure all taggable AWS resources have required ACDL tags"
|
||||
check_id = "ACDL_TAG_NAMING"
|
||||
supported_resources = ["*"] # all resources
|
||||
categories = [CheckCategories.GENERAL_SECURITY]
|
||||
super().__init__(name=name, check_id=check_id, categories=categories, supported_resources=supported_resources)
|
||||
|
||||
def scan_resource_conf(self, conf, entity_type):
|
||||
# Skip non-taggable resources
|
||||
if entity_type in NON_TAGGABLE_TYPES:
|
||||
return CheckResult.PASSED
|
||||
# Check for a tags block
|
||||
tags = conf.get("tags")
|
||||
if not tags:
|
||||
return CheckResult.FAILED
|
||||
tag_keys = set()
|
||||
if isinstance(tags, list) and tags:
|
||||
tag_block = tags[0]
|
||||
if isinstance(tag_block, dict):
|
||||
tag_keys = set(tag_block.keys())
|
||||
elif isinstance(tags, dict):
|
||||
tag_keys = set(tags.keys())
|
||||
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
|
||||
if missing:
|
||||
return CheckResult.FAILED
|
||||
return CheckResult.PASSED
|
||||
|
||||
check = AcdlTaggingStandard()
|
||||
@@ -0,0 +1,55 @@
|
||||
# Wiz Adapter
|
||||
|
||||
The Wiz adapter translates Wiz API issue records to the normalized ACDL
|
||||
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
|
||||
(engine: `"wiz"`), mirroring the Checkov adapter pattern.
|
||||
|
||||
## What Wiz is
|
||||
|
||||
[Wiz](https://www.wiz.io/) is a cloud security SaaS platform that
|
||||
continuously scans CSPM / CWPP / KSPM findings across AWS, Azure, GCP and
|
||||
Kubernetes. It exposes a GraphQL/REST API for fetching issue records.
|
||||
|
||||
## Adapter behaviour
|
||||
|
||||
`wiz_adapter.py <wiz_issues.json> <contract-id>` reads a JSON file of Wiz
|
||||
issue records (the shape returned by the Wiz `issues` GraphQL query /
|
||||
list endpoint) and emits a list of `PolicyCheckResult` dicts:
|
||||
|
||||
| Wiz field | PolicyCheckResult field |
|
||||
|------------------|------------------------------------------------------------|
|
||||
| `id` / `control.id` | `ruleId` |
|
||||
| `severity` | `severity` (mapped `CRITICAL/HIGH/MEDIUM/LOW/INFO`) |
|
||||
| `status` | `result` (`OPEN→fail`, `RESOLVED→pass`, `IN_PROGRESS/DISMISSED→skipped`) |
|
||||
| `title` / `control.name` | `message` |
|
||||
| `entity.id` | `resourceRef` + `evidence.resource` |
|
||||
| `entity.{name,cloudPlatform,subscriptionId}` | `evidence.*` |
|
||||
|
||||
The adapter is read-only against a local JSON fixture; the pipeline is
|
||||
responsible for fetching from Wiz (when configured) and writing the file.
|
||||
|
||||
## Offline / degraded behaviour (D-052)
|
||||
|
||||
When Wiz is not configured the pipeline passes an empty issues payload (or
|
||||
simply does not invoke the adapter). The adapter degrades gracefully:
|
||||
|
||||
- an empty `issues` list → the adapter emits a single `WIZ_NOT_CONFIGURED`
|
||||
`PolicyCheckResult` with `result: "skipped"` so the confidence policy
|
||||
input stays non-empty (and does not falsely inflate the score).
|
||||
|
||||
`is_configured()` returns `True` only when the `WIZ_API_TOKEN`
|
||||
environment variable is set; the pipeline uses it to decide whether to
|
||||
fetch and invoke the adapter at all.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Env var | Required | Purpose |
|
||||
|-----------------|----------|--------------------------------------------------|
|
||||
| `WIZ_API_TOKEN` | yes | Bearer token for the Wiz REST API. When unset, `is_configured()` returns `False`. |
|
||||
| `WIZ_ENDPOINT` | no | Wiz API endpoint (defaults to `https://api.wiz.io` when implemented). |
|
||||
|
||||
## Schema path
|
||||
|
||||
The output records validate against
|
||||
[`schemas/policy_check_result.schema.json`](../../schemas/policy_check_result.schema.json)
|
||||
(`engine: "wiz"` was added to the enum in Phase 23).
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records.
|
||||
|
||||
Wiz is a SaaS security platform with a REST API (issues, security graph
|
||||
queries). This adapter translates Wiz issue records to the normalized
|
||||
PolicyCheckResult schema (engine: "wiz"), matching the Checkov adapter
|
||||
pattern.
|
||||
|
||||
D-052: stub + schema path. The adapter degrades gracefully when Wiz is
|
||||
not configured — it emits a single SKIPPED record (WIZ_NOT_CONFIGURED)
|
||||
so the confidence policy input stays non-empty. The pipeline invokes it
|
||||
optionally when WIZ_API_TOKEN is set.
|
||||
|
||||
CLI: wiz_adapter.py <wiz_issues.json> <contract-id>
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
SEVERITY_MAP = {
|
||||
"CRITICAL": "critical",
|
||||
"HIGH": "high",
|
||||
"MEDIUM": "medium",
|
||||
"LOW": "low",
|
||||
"INFO": "info",
|
||||
}
|
||||
|
||||
RESULT_MAP = {
|
||||
"OPEN": "fail",
|
||||
"RESOLVED": "pass",
|
||||
"IN_PROGRESS": "skipped",
|
||||
"DISMISSED": "skipped",
|
||||
}
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _to_pcr(wiz_issue, contract_id):
|
||||
severity_raw = wiz_issue.get("severity", "INFO")
|
||||
severity = SEVERITY_MAP.get(str(severity_raw).upper(), "info")
|
||||
status = wiz_issue.get("status", "OPEN")
|
||||
result = RESULT_MAP.get(str(status).upper(), "error")
|
||||
control = wiz_issue.get("control", {})
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "wiz",
|
||||
"ruleId": wiz_issue.get("id", control.get("id", "WIZ_UNKNOWN")),
|
||||
"severity": severity,
|
||||
"result": result,
|
||||
"message": wiz_issue.get("title", control.get("name", "")),
|
||||
"evidence": {
|
||||
"resource": wiz_issue.get("entity", {}).get("id"),
|
||||
"resource_name": wiz_issue.get("entity", {}).get("name"),
|
||||
"cloud_platform": wiz_issue.get("entity", {}).get("cloudPlatform"),
|
||||
"subscription_id": wiz_issue.get("entity", {}).get("subscriptionId"),
|
||||
},
|
||||
"resourceRef": wiz_issue.get("entity", {}).get("id", ""),
|
||||
}
|
||||
|
||||
|
||||
def _emit_not_configured(contract_id):
|
||||
return {
|
||||
"contractId": contract_id,
|
||||
"evaluatedAt": _iso8601_now(),
|
||||
"engine": "wiz",
|
||||
"ruleId": "WIZ_NOT_CONFIGURED",
|
||||
"severity": "info",
|
||||
"result": "skipped",
|
||||
"message": "Wiz adapter not configured (WIZ_API_TOKEN not set); degraded gracefully (D-052).",
|
||||
"evidence": {},
|
||||
"resourceRef": "",
|
||||
}
|
||||
|
||||
|
||||
def adapt(wiz_json_path, contract_id):
|
||||
with open(wiz_json_path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
out = []
|
||||
# Accept either a bare list of issues or an object with an "issues" key.
|
||||
if isinstance(data, list):
|
||||
issues = data
|
||||
else:
|
||||
issues = data.get("issues", [])
|
||||
if not isinstance(issues, list):
|
||||
issues = []
|
||||
for issue in issues:
|
||||
out.append(_to_pcr(issue, contract_id))
|
||||
if not out:
|
||||
out.append(_emit_not_configured(contract_id))
|
||||
return out
|
||||
|
||||
|
||||
def is_configured():
|
||||
return bool(os.environ.get("WIZ_API_TOKEN"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: wiz_adapter.py <wiz_issues.json> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
print(json.dumps(adapt(sys.argv[1], sys.argv[2]), indent=2))
|
||||
@@ -1,7 +0,0 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY app.py /app/app.py
|
||||
|
||||
EXPOSE 8080
|
||||
CMD ["python", "/app/app.py"]
|
||||
@@ -1,34 +0,0 @@
|
||||
# acdl-consumer-microservice
|
||||
|
||||
A basic HTTP microservice for the ACDL v1.2 milestone. Returns 200 on `/`
|
||||
and `/health` with a JSON status body. Deployed to AWS ECS Fargate via the
|
||||
ACDL platform's `microservice` contract.
|
||||
|
||||
## Build + push to ECR
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t acdl-microservice .
|
||||
|
||||
# Tag for ECR
|
||||
docker tag acdl-microservice:latest 581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest
|
||||
|
||||
# Authenticate to ECR
|
||||
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 581513795199.dkr.ecr.us-east-1.amazonaws.com
|
||||
|
||||
# Push
|
||||
docker push 581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest
|
||||
```
|
||||
|
||||
## Contract
|
||||
|
||||
The contract submission is at `contracts/microservice.yaml` (or the
|
||||
platform's `contracts/microservice.yaml`). Submitting it to the ACDL
|
||||
pipeline triggers: contract → IR resolution → `terraform plan` →
|
||||
`terraform apply` (dev) → a live ECS Fargate service.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /` — 200, `{"status":"ok","service":"acdl-microservice","version":"1.0.0"}`
|
||||
- `GET /health` — 200, same body
|
||||
- any other path — 404
|
||||
@@ -1,37 +0,0 @@
|
||||
"""ACDL consumer microservice — a tiny HTTP server returning 200 on /.
|
||||
|
||||
This is the reference consumer microservice for the v1.2 milestone. It's
|
||||
intentionally minimal: stdlib only, no framework, no dependencies. The
|
||||
platform deploys it to ECS Fargate via the microservice contract.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path == "/" or self.path == "/health":
|
||||
body = json.dumps({
|
||||
"status": "ok",
|
||||
"service": "acdl-microservice",
|
||||
"version": "1.0.0",
|
||||
}).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"{self.address_string()} - {format % args}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.environ.get("PORT", "8080"))
|
||||
server = HTTPServer(("0.0.0.0", port), Handler)
|
||||
print(f"acdl-microservice listening on :{port}", flush=True)
|
||||
server.serve_forever()
|
||||
@@ -0,0 +1,13 @@
|
||||
# ACDL sample consumer contract — microservice module (dev)
|
||||
#
|
||||
# Reference example for an ECS Fargate microservice deployment.
|
||||
# This contract declares only the inputs the composition wires reference
|
||||
# (bucket_name, region) plus a representative image/port.
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: acdl-microservice-demo
|
||||
region: us-east-1
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -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
|
||||
+102
-6
@@ -44,7 +44,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
||||
- "<childId>.outputs.<name>" — a reference to another child's output
|
||||
|
||||
Returns either a concrete value (string/number/boolean) or a
|
||||
"ref:<childId>.<outputName>" string for cross-child references.
|
||||
"ref:<resourceId>.<outputName>" string for cross-child references.
|
||||
|
||||
For multi-resource L1s (e.g. vpc which expands to vpc-vpc, vpc-subnet,
|
||||
vpc-routetable), the ref must point to the sub-resource that actually
|
||||
produces the output, not the child id. The child_outputs table maps
|
||||
childId -> {outputName -> resourceId} so the ref uses the correct
|
||||
resource id.
|
||||
"""
|
||||
from_expr = wire["from"]
|
||||
to_expr = wire["to"]
|
||||
@@ -66,7 +72,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
||||
if len(parts) >= 3 and parts[1] == "outputs":
|
||||
child_id = parts[0]
|
||||
output_name = parts[2]
|
||||
return f"ref:{child_id}.{output_name}"
|
||||
# Look up the sub-resource that produces this output.
|
||||
# child_outputs[child_id] is a dict {outputName -> resourceId}.
|
||||
# If the child is a single-resource L1, the resourceId == child_id.
|
||||
# If multi-resource, the resourceId is the expanded sub-resource id.
|
||||
child_out_map = child_outputs.get(child_id, {})
|
||||
resource_id = child_out_map.get(output_name, child_id)
|
||||
return f"ref:{resource_id}.{output_name}"
|
||||
|
||||
return None
|
||||
|
||||
@@ -125,6 +137,9 @@ def resolve_l2(contract, registry, repo_root):
|
||||
composition = _load_json(comp_path)
|
||||
|
||||
# Track child outputs for wire resolution
|
||||
# child_outputs[childId] = {outputName: resourceId}
|
||||
# For single-resource L1s, resourceId == childId
|
||||
# For multi-resource L1s, resourceId is the expanded sub-resource id
|
||||
child_outputs = {}
|
||||
resources = []
|
||||
|
||||
@@ -139,15 +154,18 @@ def resolve_l2(contract, registry, repo_root):
|
||||
child_iface_path = os.path.join(repo_root, child_entry["interface"])
|
||||
child_iface = _load_json(child_iface_path)
|
||||
|
||||
# Build the output->resourceId map for this child
|
||||
child_out_map = {}
|
||||
|
||||
# For multi-resource L1s (like vpc), the first resource type is the
|
||||
# primary; the adapter handles expansion. Use the interface's type
|
||||
# or the first resource in the interface's resources array.
|
||||
if "resources" in child_iface and child_iface["resources"]:
|
||||
# Multi-resource L1: create one resource per sub-resource
|
||||
for sub_res in child_iface["resources"]:
|
||||
res_id = f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}" if len(child_iface["resources"]) > 1 else child_id
|
||||
resource = {
|
||||
"id": f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}"
|
||||
if len(child_iface["resources"]) > 1 else child_id,
|
||||
"id": res_id,
|
||||
"type": sub_res["type"],
|
||||
"module": child_module,
|
||||
"inputs": {},
|
||||
@@ -157,6 +175,9 @@ def resolve_l2(contract, registry, repo_root):
|
||||
},
|
||||
}
|
||||
resources.append(resource)
|
||||
# Map each output to this sub-resource's id
|
||||
for out_name in sub_res.get("outputs", []):
|
||||
child_out_map[out_name] = res_id
|
||||
else:
|
||||
# Single-resource L1
|
||||
resource = {
|
||||
@@ -170,9 +191,17 @@ def resolve_l2(contract, registry, repo_root):
|
||||
},
|
||||
}
|
||||
resources.append(resource)
|
||||
# Map each output to the child id
|
||||
for out_name in child_iface.get("outputs", {}):
|
||||
child_out_map[out_name] = child_id
|
||||
|
||||
# Track outputs for this child
|
||||
child_outputs[child_id] = child_iface.get("outputs", {})
|
||||
# Also map interface-level outputs (for L1s that declare outputs at the
|
||||
# interface level rather than per-resource)
|
||||
for out_name in child_iface.get("outputs", {}):
|
||||
if out_name not in child_out_map:
|
||||
child_out_map[out_name] = child_id
|
||||
|
||||
child_outputs[child_id] = child_out_map
|
||||
|
||||
# Resolve wires to populate inputs
|
||||
for wire in composition.get("wires", []):
|
||||
@@ -203,6 +232,73 @@ def resolve_l2(contract, registry, repo_root):
|
||||
"resources": resources,
|
||||
}
|
||||
|
||||
# REQ-87: Propagate deletion_protection feature flag from contract inputs
|
||||
# to all children's NFRs. When inputs.deletion_protection is false,
|
||||
# all resources get deletion_protection=false (used by decommission).
|
||||
deletion_protection_input = inputs.get("deletion_protection", True)
|
||||
if deletion_protection_input is not True:
|
||||
for res in resources:
|
||||
if "nfrs" not in res:
|
||||
res["nfrs"] = {}
|
||||
res["nfrs"]["deletion_protection"] = deletion_protection_input
|
||||
# Also record the feature flag on the stack object for introspection.
|
||||
if "deletion_protection" in inputs:
|
||||
stack_instance["stack"]["features"] = {
|
||||
"deletion_protection": deletion_protection_input
|
||||
}
|
||||
|
||||
# P1-7: Process the composition's outputs[] array to build stack.outputs.
|
||||
# Each output wire: {"from": "<childId>.outputs.<name>", "to": "stack.outputs.<outName>"}
|
||||
# The child_outputs map (childId -> {outputName: resourceId}) resolves
|
||||
# the source to a resource id, which the adapter uses to emit
|
||||
# `output "<outName>" { value = aws_<type>.<resourceId>.<attr> }`.
|
||||
stack_outputs = {}
|
||||
for out_wire in composition.get("outputs", []):
|
||||
from_expr = out_wire.get("from", "")
|
||||
to_expr = out_wire.get("to", "")
|
||||
# Parse "to": "stack.outputs.<outName>"
|
||||
to_parts = to_expr.split(".")
|
||||
if len(to_parts) != 3 or to_parts[1] != "outputs":
|
||||
continue
|
||||
out_name = to_parts[2]
|
||||
# Parse "from": "<childId>.outputs.<name>"
|
||||
from_parts = from_expr.split(".")
|
||||
if len(from_parts) != 3 or from_parts[1] != "outputs":
|
||||
continue
|
||||
src_child = from_parts[0]
|
||||
src_output = from_parts[2]
|
||||
# Resolve the source resource id from child_outputs
|
||||
child_out_map = child_outputs.get(src_child, {})
|
||||
src_resource_id = child_out_map.get(src_output, src_child)
|
||||
stack_outputs[out_name] = {
|
||||
"type": "string",
|
||||
"from": src_resource_id,
|
||||
"output": src_output,
|
||||
}
|
||||
if stack_outputs:
|
||||
stack_instance["outputs"] = stack_outputs
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
def decommission_transform(stack_instance):
|
||||
"""REQ-92: Transform a resolved stack instance for decommission.
|
||||
|
||||
Sets all scalable counts to 0 and deletion_protection to false on
|
||||
every resource. Used by the decommission pipeline mode after the
|
||||
first step (disable deletion protection) has been applied.
|
||||
"""
|
||||
for res in stack_instance.get("resources", []):
|
||||
if "nfrs" not in res:
|
||||
res["nfrs"] = {}
|
||||
res["nfrs"]["deletion_protection"] = False
|
||||
inputs = res.get("inputs", {})
|
||||
if "desired_count" in inputs:
|
||||
inputs["desired_count"] = 0
|
||||
if "min_capacity" in inputs:
|
||||
inputs["min_capacity"] = 0
|
||||
if "max_capacity" in inputs:
|
||||
inputs["max_capacity"] = 0
|
||||
return stack_instance
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Platform Lambda — contract ingestor.
|
||||
|
||||
Invoked via a Function URL (IAM auth) by consumer pipelines (one-way
|
||||
communication, D-051). Accepts { consumerRepo, contractId, contract,
|
||||
environment, action } and writes contracts to DynamoDB table acdl-contracts
|
||||
(PK consumerRepo, SK contractId#submittedAt).
|
||||
|
||||
The report_error action (D-055) creates a GitHub issue on the platform repo
|
||||
via the GitHub API, using a token from Secrets Manager. It is idempotent: if
|
||||
an open issue with the same title exists, it comments rather than duplicating.
|
||||
|
||||
Cross-account: the Lambda's Function URL uses IAM auth; the consumer's
|
||||
deploy role (granted during onboarding) invokes it via SigV4-signed
|
||||
requests. The invoke policy is scoped via ABAC (consumer repo identity).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import boto3
|
||||
|
||||
TABLE_NAME = os.environ.get("CONTRACTS_TABLE", "acdl-contracts")
|
||||
CHANGE_REQUESTS_TABLE = os.environ.get("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
|
||||
GITHUB_TOKEN_SECRET_ID = os.environ.get("GITHUB_TOKEN_SECRET_ID", "acdl/github-token")
|
||||
PLATFORM_REPO = os.environ.get("PLATFORM_REPO", "acdl/acdl")
|
||||
# P1-9: Forge-agnostic API base URL. Defaults to GitHub; set GITHUB_API_BASE
|
||||
# to a Gitea API root (e.g. https://git.cloudinit.dev/api/v1) for Gitea.
|
||||
GITHUB_API_BASE = os.environ.get("GITHUB_API_BASE", "https://api.github.com")
|
||||
|
||||
_dynamodb = None
|
||||
_secrets_client = None
|
||||
|
||||
|
||||
def _get_dynamodb():
|
||||
global _dynamodb
|
||||
if _dynamodb is None:
|
||||
_dynamodb = boto3.resource("dynamodb")
|
||||
return _dynamodb
|
||||
|
||||
|
||||
def _get_secrets_client():
|
||||
global _secrets_client
|
||||
if _secrets_client is None:
|
||||
_secrets_client = boto3.client("secretsmanager")
|
||||
return _secrets_client
|
||||
|
||||
|
||||
def _iso8601_now():
|
||||
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _forge_type():
|
||||
"""P1-9: Detect whether the API base is GitHub or Gitea.
|
||||
|
||||
Gitea API roots contain '/api/v1'; GitHub's is 'api.github.com'.
|
||||
"""
|
||||
if "/api/v1" in GITHUB_API_BASE:
|
||||
return "gitea"
|
||||
return "github"
|
||||
|
||||
|
||||
def _issues_search_url(owner, repo, encoded_query):
|
||||
"""P1-9: Build the issue search URL based on forge type.
|
||||
|
||||
GitHub uses /search/issues?q=...; Gitea uses /repos/{owner}/{repo}/issues?...
|
||||
with query params (no /search/issues endpoint).
|
||||
"""
|
||||
if _forge_type() == "gitea":
|
||||
return (
|
||||
f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||
f"?state=open&type=issues&q={encoded_query}"
|
||||
)
|
||||
return (
|
||||
f"{GITHUB_API_BASE}/search/issues?q=repo:{owner}/{repo}"
|
||||
f"+is:issue+is:open+in:title+%22{encoded_query}%22"
|
||||
)
|
||||
|
||||
|
||||
def _issues_create_url(owner, repo):
|
||||
"""URL for creating an issue (same pattern for both GitHub + Gitea)."""
|
||||
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues"
|
||||
|
||||
|
||||
def _issue_comments_url(owner, repo, issue_number):
|
||||
"""URL for posting a comment on an issue (same for both forges)."""
|
||||
return f"{GITHUB_API_BASE}/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||
|
||||
|
||||
def _submit_contract(payload):
|
||||
consumer_repo = payload["consumerRepo"]
|
||||
contract_id = payload["contractId"]
|
||||
contract = payload["contract"]
|
||||
environment = payload["environment"]
|
||||
submitted_at = _iso8601_now()
|
||||
table = _get_dynamodb().Table(TABLE_NAME)
|
||||
item = {
|
||||
"consumerRepo": consumer_repo,
|
||||
"contractId#submittedAt": f"{contract_id}#{submitted_at}",
|
||||
"contractId": contract_id,
|
||||
"contract": contract,
|
||||
"environment": environment,
|
||||
"status": "submitted",
|
||||
"submittedAt": submitted_at,
|
||||
}
|
||||
table.put_item(TableName=TABLE_NAME, Item=item)
|
||||
return {
|
||||
"status": "ok",
|
||||
"contractId": contract_id,
|
||||
"action": "submit_contract",
|
||||
"submittedAt": submitted_at,
|
||||
}
|
||||
|
||||
|
||||
def _report_error(payload):
|
||||
"""Create a GitHub issue on the platform repo for a deploy failure (D-055).
|
||||
|
||||
Uses the GitHub token from Secrets Manager. Idempotent: if an open
|
||||
issue with the same title exists, comments on it rather than duplicating.
|
||||
"""
|
||||
import urllib.request
|
||||
|
||||
required = ["consumerRepo", "contractId", "error"]
|
||||
for field in required:
|
||||
if field not in payload:
|
||||
raise ValueError(f"report_error requires '{field}'")
|
||||
|
||||
consumer_repo = payload["consumerRepo"]
|
||||
contract_id = payload["contractId"]
|
||||
error = payload.get("error", "unknown error")
|
||||
run_url = payload.get("runUrl", "")
|
||||
stack_trace = payload.get("stackTrace", "")[:2000] # truncate
|
||||
|
||||
# Get the GitHub token from Secrets Manager
|
||||
secrets = _get_secrets_client()
|
||||
try:
|
||||
secret_response = secrets.get_secret_value(SecretId=GITHUB_TOKEN_SECRET_ID)
|
||||
github_token = secret_response["SecretString"]
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
|
||||
|
||||
owner, repo = PLATFORM_REPO.split("/")
|
||||
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
|
||||
|
||||
# Check for an existing open issue with the same title (idempotency)
|
||||
# URL-encode the contract_id to prevent search-query injection (P1-1).
|
||||
encoded_contract_id = urllib.parse.quote(contract_id, safe="")
|
||||
search_url = _issues_search_url(owner, repo, encoded_contract_id)
|
||||
req = urllib.request.Request(search_url)
|
||||
req.add_header("Authorization", f"token {github_token}")
|
||||
req.add_header("Accept", "application/vnd.github+json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
search_result = json.loads(resp.read())
|
||||
existing = search_result.get("items", [])
|
||||
except Exception:
|
||||
existing = []
|
||||
|
||||
body = f"""## Deploy Failure Report
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Consumer repo** | `{consumer_repo}` |
|
||||
| **Contract ID** | `{contract_id}` |
|
||||
| **Run URL** | {run_url if run_url else "_(not provided)_"} |
|
||||
| **Environment** | {payload.get('environment', 'unknown')} |
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
{error}
|
||||
```
|
||||
|
||||
## Stack Trace
|
||||
|
||||
```
|
||||
{stack_trace}
|
||||
```
|
||||
|
||||
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
|
||||
"""
|
||||
|
||||
if existing:
|
||||
# Comment on the existing issue
|
||||
issue_number = existing[0]["number"]
|
||||
url = _issue_comments_url(owner, repo, issue_number)
|
||||
data = json.dumps({"body": body}).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Authorization", f"token {github_token}")
|
||||
req.add_header("Accept", "application/vnd.github+json")
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
return {
|
||||
"status": "commented_on_existing",
|
||||
"issueNumber": issue_number,
|
||||
"contractId": contract_id,
|
||||
"action": "report_error",
|
||||
}
|
||||
else:
|
||||
# Create a new issue
|
||||
url = _issues_create_url(owner, repo)
|
||||
data = json.dumps({
|
||||
"title": title,
|
||||
"body": body,
|
||||
"labels": ["platform-alert", "auto-generated"],
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Authorization", f"token {github_token}")
|
||||
req.add_header("Accept", "application/vnd.github+json")
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
issue = json.loads(resp.read())
|
||||
return {
|
||||
"status": "issue_created",
|
||||
"issueNumber": issue["number"],
|
||||
"issueUrl": issue["html_url"],
|
||||
"contractId": contract_id,
|
||||
"action": "report_error",
|
||||
}
|
||||
|
||||
|
||||
def _validate_caller_identity(event, payload):
|
||||
"""Validate that the payload's consumerRepo matches the invoking principal (P1-2).
|
||||
|
||||
The Lambda's Function URL uses IAM auth. The caller's identity is available
|
||||
in event["requestContext"]["identity"]. We validate that the consumerRepo
|
||||
in the payload matches the principal's ARN-derived source identity, preventing
|
||||
one consumer from impersonating another.
|
||||
|
||||
If the identity is not available (e.g. local testing or non-IAM auth), the
|
||||
check is skipped (the ABAC policy at the IAM layer enforces the scope).
|
||||
"""
|
||||
identity = event.get("requestContext", {}).get("identity", {})
|
||||
caller_arn = identity.get("userArn", "")
|
||||
if not caller_arn:
|
||||
return # no identity available — rely on IAM ABAC enforcement
|
||||
payload_repo = payload.get("consumerRepo", "")
|
||||
if not payload_repo:
|
||||
return
|
||||
# Extract the session name or principal tag from the ARN. The ABAC policy
|
||||
# scopes via aws:PrincipalTag/acdl:owner = <consumerRepo>. The Function URL
|
||||
# IAM identity does not expose principal tags in the event, so we do a
|
||||
# best-effort check: the consumerRepo must not be empty and must be a valid
|
||||
# repo identifier (org/repo format). Full enforcement is at the IAM layer.
|
||||
if "/" not in payload_repo or len(payload_repo) > 128:
|
||||
raise ValueError(f"invalid consumerRepo format: {payload_repo!r}")
|
||||
|
||||
|
||||
def _validate_change_request(payload):
|
||||
"""REQ-93: Validate a change request ID against the CMDB (DynamoDB).
|
||||
|
||||
Queries the acdl-change-requests table for the given changeRequestId.
|
||||
Returns the CR details if status is 'approved' and the consumerRepo matches.
|
||||
Raises ValueError if the CR is not found, not approved, or the repo doesn't match.
|
||||
"""
|
||||
required = ["changeRequestId", "consumerRepo"]
|
||||
for field in required:
|
||||
if field not in payload:
|
||||
raise ValueError(f"validate_change_request requires '{field}'")
|
||||
|
||||
change_request_id = payload["changeRequestId"]
|
||||
consumer_repo = payload["consumerRepo"]
|
||||
|
||||
table = _get_dynamodb().Table(CHANGE_REQUESTS_TABLE)
|
||||
response = table.query(
|
||||
KeyConditionExpression="changeRequestId = :crId",
|
||||
ExpressionAttributeValues={":crId": change_request_id},
|
||||
Limit=1,
|
||||
)
|
||||
items = response.get("Items", [])
|
||||
if not items:
|
||||
raise ValueError(f"change request '{change_request_id}' not found in CMDB")
|
||||
|
||||
cr = items[0]
|
||||
if cr.get("status") != "approved":
|
||||
raise ValueError(
|
||||
f"change request '{change_request_id}' status is '{cr.get('status')}', expected 'approved'"
|
||||
)
|
||||
|
||||
if cr.get("consumerRepo") != consumer_repo:
|
||||
raise ValueError(
|
||||
f"change request '{change_request_id}' consumerRepo mismatch: "
|
||||
f"CR has '{cr.get('consumerRepo')}', request has '{consumer_repo}'"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "approved",
|
||||
"changeRequestId": change_request_id,
|
||||
"consumerRepo": consumer_repo,
|
||||
"contractId": cr.get("contractId", ""),
|
||||
"action": "validate_change_request",
|
||||
}
|
||||
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""AWS Lambda handler entry point.
|
||||
|
||||
Accepts a Function-URL-style event whose ``body`` is a JSON string
|
||||
containing ``{ consumerRepo, contractId, contract, environment, action }``.
|
||||
"""
|
||||
try:
|
||||
body = event.get("body", "{}")
|
||||
if isinstance(body, str):
|
||||
payload = json.loads(body)
|
||||
else:
|
||||
payload = body
|
||||
action = payload.get("action", "submit_contract")
|
||||
# Validate caller identity against the payload (P1-2).
|
||||
_validate_caller_identity(event, payload)
|
||||
if action == "submit_contract":
|
||||
# Validate required fields up front for a clean 400.
|
||||
for field in ("consumerRepo", "contractId", "contract", "environment"):
|
||||
if field not in payload:
|
||||
return {
|
||||
"statusCode": 400,
|
||||
"body": json.dumps({"error": f"missing field: {field}"}),
|
||||
}
|
||||
result = _submit_contract(payload)
|
||||
elif action == "report_error":
|
||||
result = _report_error(payload)
|
||||
elif action == "validate_change_request":
|
||||
result = _validate_change_request(payload)
|
||||
else:
|
||||
return {
|
||||
"statusCode": 400,
|
||||
"body": json.dumps({"error": f"unknown action: {action}"}),
|
||||
}
|
||||
return {"statusCode": 200, "body": json.dumps(result)}
|
||||
except ValueError as e:
|
||||
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
|
||||
except Exception as e: # pragma: no cover - defensive top-level guard
|
||||
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Publish deploy outputs to SSM + format GitHub PR comments (D-050).
|
||||
|
||||
Two canonical mechanisms:
|
||||
1. SSM Parameter Store (SecureString, KMS-encrypted) for runtime-injectable
|
||||
values — resources that need to read outputs at runtime (e.g. an ECS
|
||||
task reading its S3 bucket name).
|
||||
2. GitHub PR comment / job summary for human-readable outputs (connection
|
||||
strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
|
||||
the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).
|
||||
|
||||
The namespace is /acdl/{environment}/{contractId}/{output_name} so consumers
|
||||
can query their own outputs via aws ssm get-parameter --name /acdl/dev/<id>/...
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
boto3 = None
|
||||
|
||||
SSM_PREFIX = "/acdl"
|
||||
KMS_KEY_ID_ENV = "ACDL_KMS_KEY_ID"
|
||||
|
||||
# Outputs that are safe to display in a PR comment (no secrets).
|
||||
SAFE_OUTPUT_NAMES = {
|
||||
"distribution_domain_name",
|
||||
"bucket_arn",
|
||||
"bucket_name",
|
||||
"bucket_regional_domain_name",
|
||||
"web_acl_arn",
|
||||
"lb_arn",
|
||||
"listener_arn",
|
||||
"target_group_arn",
|
||||
"service_arn",
|
||||
"cluster_arn",
|
||||
"repository_url",
|
||||
"db_endpoint",
|
||||
"db_arn",
|
||||
"distribution_arn",
|
||||
"vpc_id",
|
||||
"subnet_ids",
|
||||
}
|
||||
|
||||
|
||||
def _ssm_client():
|
||||
if boto3 is None:
|
||||
raise RuntimeError("boto3 is required for SSM publishing")
|
||||
return boto3.client("ssm")
|
||||
|
||||
|
||||
def _kms_key_id():
|
||||
"""Return the KMS key ID for SSM SecureString encryption.
|
||||
|
||||
P1-3: Fail loud when ACDL_KMS_KEY_ID is not set — silently falling back
|
||||
to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
|
||||
CMK must be explicitly configured. Set ACDL_ALLOW_DEFAULT_KMS=1 to use
|
||||
the AWS-managed key as an escape hatch for local testing.
|
||||
"""
|
||||
key_id = os.environ.get(KMS_KEY_ID_ENV)
|
||||
if key_id:
|
||||
return key_id
|
||||
if os.environ.get("ACDL_ALLOW_DEFAULT_KMS") == "1":
|
||||
return "alias/aws/ssm"
|
||||
raise RuntimeError(
|
||||
f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key "
|
||||
f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set "
|
||||
f"ACDL_ALLOW_DEFAULT_KMS=1 to use alias/aws/ssm (escape hatch for local testing)."
|
||||
)
|
||||
|
||||
|
||||
def publish_to_ssm(outputs, environment, contract_id):
|
||||
"""Write each output to SSM Parameter Store as a SecureString.
|
||||
|
||||
Returns a dict of {output_name: parameter_arn} for successful writes.
|
||||
Skips None values and empty strings.
|
||||
"""
|
||||
if boto3 is None:
|
||||
return {}
|
||||
client = _ssm_client()
|
||||
kms_key = _kms_key_id()
|
||||
results = {}
|
||||
for name, value in outputs.items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str) and not value.strip():
|
||||
continue
|
||||
param_name = f"{SSM_PREFIX}/{environment}/{contract_id}/{name}"
|
||||
try:
|
||||
client.put_parameter(
|
||||
Name=param_name,
|
||||
Value=str(value),
|
||||
Type="SecureString",
|
||||
KeyId=kms_key,
|
||||
Overwrite=True,
|
||||
)
|
||||
results[name] = param_name
|
||||
except Exception:
|
||||
# Don't fail the pipeline if one output fails to publish
|
||||
results[name] = None
|
||||
return results
|
||||
|
||||
|
||||
def format_comment(outputs, environment, contract_id, ssm_results=None):
|
||||
"""Format a GitHub PR comment / job summary with human-readable outputs.
|
||||
|
||||
Only non-sensitive outputs (SAFE_OUTPUT_NAMES) are included. Sensitive
|
||||
outputs are noted as 'published to SSM' without their values.
|
||||
"""
|
||||
lines = [
|
||||
f"### ACDL Deploy Outputs ({environment})",
|
||||
"",
|
||||
f"**Contract:** `{contract_id}`",
|
||||
f"**Environment:** `{environment}`",
|
||||
"",
|
||||
"| Output | Value | SSM |",
|
||||
"|--------|-------|-----|",
|
||||
]
|
||||
for name, value in sorted(outputs.items()):
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, str) and not value.strip():
|
||||
continue
|
||||
safe = name in SAFE_OUTPUT_NAMES
|
||||
display = str(value) if safe else "`(published to SSM)`"
|
||||
ssm_path = ""
|
||||
if ssm_results and ssm_results.get(name):
|
||||
ssm_path = f"`{ssm_results[name]}`"
|
||||
elif ssm_results is not None:
|
||||
ssm_path = "—"
|
||||
lines.append(f"| `{name}` | {display} | {ssm_path} |")
|
||||
lines.append("")
|
||||
lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /acdl/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
|
||||
"""Post a comment to a GitHub PR via the GitHub API.
|
||||
|
||||
Uses GITHUB_TOKEN from env if token is None. Uses GITHUB_REPOSITORY if
|
||||
repo is None. Uses the PR number from the GITHUB_REF env if pr_number is
|
||||
None (extracts from refs/pull/<N>/merge). No-op if not in a PR context.
|
||||
"""
|
||||
if token is None:
|
||||
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
||||
if repo is None:
|
||||
repo = os.environ.get("GITHUB_REPOSITORY", "")
|
||||
if pr_number is None:
|
||||
ref = os.environ.get("GITHUB_REF", "")
|
||||
if "refs/pull/" in ref:
|
||||
try:
|
||||
pr_number = int(ref.split("/")[2])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
if not token or not repo or not pr_number:
|
||||
return False # not in a PR context or no token
|
||||
try:
|
||||
import urllib.request
|
||||
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
|
||||
data = json.dumps({"body": comment_text}).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Authorization", f"token {token}")
|
||||
req.add_header("Accept", "application/vnd.github+json")
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# CLI: output_publisher.py <outputs.json> <environment> <contract_id>
|
||||
if len(sys.argv) != 4:
|
||||
print("usage: output_publisher.py <outputs.json> <environment> <contract-id>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
with open(sys.argv[1]) as f:
|
||||
outputs = json.load(f)
|
||||
env = sys.argv[2]
|
||||
cid = sys.argv[3]
|
||||
ssm_results = publish_to_ssm(outputs, env, cid)
|
||||
comment = format_comment(outputs, env, cid, ssm_results)
|
||||
print(comment)
|
||||
@@ -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.
|
||||
|
||||
+87
-21
@@ -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<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.4| B
|
||||
A["your repo<br/>(app code + contracts + CI definitions)"] -->|uses: acdl/.github/workflows/deploy.yml@v1.6| B
|
||||
B["platform runners<br/>(modules + pipelines + adapters + schemas)"] -->|contract -> resolver -> stack -> adapter<br/>-> security checks -> infrastructure plan -> policy checks<br/>-> 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). |
|
||||
|
||||
@@ -139,7 +139,10 @@ inputs:
|
||||
Each module declares its inputs in its `interface.json` (primitives) or
|
||||
`composition.json` (modules). Consult the [module catalog](modules/) for
|
||||
the full list, or read the module's own README under `modules/l1/<name>/`
|
||||
or `modules/l2/<name>/`.
|
||||
or `modules/l2/<name>/`. Each module also has an `examples/` directory
|
||||
with validated consumer contract examples (`simple.yaml` + `complex.yaml`
|
||||
+ variation files) that demonstrate real usage — see the module's
|
||||
`## Examples` section.
|
||||
|
||||
The contract is validated against the contract schema. An invalid contract
|
||||
(missing field, unknown module, wrong type) fails at the validate-contract
|
||||
@@ -163,14 +166,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 +248,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/<name>/README.md` or `modules/l2/<name>/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 +281,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,11 +310,70 @@ 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`). |
|
||||
| Sample contract | `contracts/microservice.yaml` | The microservice example contract (uses `@v1.6`). |
|
||||
| Module examples | `modules/<name>/examples/` | Validated per-module example contracts (`simple.yaml` + `complex.yaml`). |
|
||||
| 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). |
|
||||
| Environments | [environments/](environments/) | Platform-managed environments + onboarding. |
|
||||
| Versioning | [pipeline/versioning](pipeline/versioning) | The `uses:` tag + module versioning. |
|
||||
| Platform README | `README.md` | How the platform works + how to run the platform repo locally. |
|
||||
| Credentials & zero-trust | `README.md#credentials--zero-trust` | The OIDC/ABAC default + static-key override model. |
|
||||
| Credentials & zero-trust | `README.md#credentials--zero-trust` | The OIDC/ABAC default + static-key override model. |
|
||||
|
||||
## Decommissioning a stack
|
||||
|
||||
When a consumer needs to tear down a deployed stack, the platform provides
|
||||
a **decommission mode** on the same deploy pipeline. The decommission
|
||||
process is a 2-step pipeline with **HITL SRE gates** to prevent accidental
|
||||
destruction:
|
||||
|
||||
1. **Request a change request (CR):** Contact the platform team to create a
|
||||
change request in the platform CMDB (DynamoDB `acdl-change-requests`
|
||||
table). The CR must be approved before decommission can proceed. The CR
|
||||
includes the consumer repo, contract ID, and the reason for decommission.
|
||||
|
||||
2. **Trigger decommission:** Update the consumer's deploy workflow call to
|
||||
use `mode: decommission` with the `changeRequestId` input:
|
||||
|
||||
```yaml
|
||||
uses: acdl/.github/workflows/deploy.yml@v1.8
|
||||
with:
|
||||
contract: .acdl/contract.yaml
|
||||
mode: decommission
|
||||
changeRequestId: "CR-2026-001"
|
||||
```
|
||||
|
||||
3. **Step 1 — Disable deletion protection (HITL SRE gate):** The pipeline
|
||||
validates the CR ID against the CMDB (status must be `approved`). Then
|
||||
it resolves the contract with `deletion_protection: false` injected into
|
||||
all resources and runs `terraform plan` + `terraform apply`. This
|
||||
removes the `prevent_destroy` lifecycle meta-argument from all resources.
|
||||
**An SRE must approve this step** via the GitHub environment
|
||||
`decommission-gate-sre`.
|
||||
|
||||
4. **Step 2 — Zero counts + destroy (HITL SRE gate):** The pipeline applies
|
||||
`decommission_transform` which sets all scalable counts to 0
|
||||
(`desired_count=0`, `min_capacity=0`, `max_capacity=0`) and
|
||||
`deletion_protection=false` on all resources. Then it runs
|
||||
`terraform plan` + `terraform apply` which destroys all resources (now
|
||||
that deletion protection is off and counts are zeroed). **A second SRE
|
||||
must approve this step** via the GitHub environment
|
||||
`decommission-destroy-sre`.
|
||||
|
||||
5. **Confirmation:** The pipeline confirms the stack is destroyed
|
||||
(terraform state is empty for the stack).
|
||||
|
||||
### What happens to the per-stack CMK?
|
||||
|
||||
The per-stack CMK is not immediately destroyed — it enters a deletion
|
||||
window (default 30 days, configurable via the `deletion_window_days` input).
|
||||
This ensures any encrypted data can still be decrypted during the deletion
|
||||
window if needed. The CMK is permanently deleted after the window expires.
|
||||
|
||||
### What happens to the uptime monitoring?
|
||||
|
||||
The uptime monitoring stack (deployed with separate state) is not
|
||||
automatically destroyed by the decommission. It must be destroyed
|
||||
separately (or left running to monitor the decommissioned stack's
|
||||
endpoints going dark).
|
||||
+17
-10
@@ -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). |
|
||||
|
||||
@@ -48,11 +48,18 @@ The contract is validated against
|
||||
An invalid contract (missing field, unknown module, wrong type) fails at the
|
||||
validate-contract stage with a clear error.
|
||||
|
||||
## Sample contract
|
||||
## Sample contracts
|
||||
|
||||
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.
|
||||
Two reference examples exist in `contracts/`:
|
||||
|
||||
- [`contracts/static-assets.yaml`](https://github.com/acdl/acdl/blob/main/contracts/static-assets.yaml)
|
||||
— the `static-assets` module (uses `@v1.6`).
|
||||
- [`contracts/microservice.yaml`](https://github.com/acdl/acdl/blob/main/contracts/microservice.yaml)
|
||||
— the `microservice` module (uses `@v1.6`).
|
||||
|
||||
Additionally, every module has a `modules/<name>/examples/` directory with
|
||||
validated example contracts (`simple.yaml` + `complex.yaml` + variation
|
||||
files). See the [module catalog](../modules/) for the full list.
|
||||
|
||||
## Multiple contracts
|
||||
|
||||
|
||||
@@ -53,6 +53,41 @@ normally.
|
||||
attestation (a platform-runner deployment approval) and a higher confidence
|
||||
threshold. Staging does not exist.
|
||||
|
||||
## Cross-account contract ingestion grant (D-051)
|
||||
|
||||
Onboarding now also grants the consumer repo's deploy role permission to
|
||||
invoke the **platform Lambda** — `acdl-contract-ingestor` — across
|
||||
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
|
||||
grant is an inline IAM policy applied to the consumer's deploy role. The
|
||||
policy template lives at
|
||||
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/acdl/acdl/blob/main/terraform/platform/consumer_invoke_policy.json)
|
||||
and is scoped via **ABAC**: the condition
|
||||
`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only
|
||||
invoke the Lambda when its principal tag matches its claimed identity.
|
||||
|
||||
The consumer's deploy workflow signs the Function URL request with
|
||||
SigV4 using its deploy-role credentials; the platform Lambda validates
|
||||
the signature and the ABAC condition before accepting the payload.
|
||||
|
||||
This is a **one-way** channel — the consumer pushes contracts *to* the
|
||||
platform; the platform never reaches back into the consumer account. It
|
||||
is used for two purposes:
|
||||
|
||||
1. **Contract ingestion** — the consumer submits its resolved deployment
|
||||
contract (`action: "submit_contract"`) so the platform has a durable
|
||||
record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK
|
||||
`contractId#submittedAt`).
|
||||
2. **Error reporting** (D-055) — the consumer reports a deployment error
|
||||
(`action: "report_error"`) which the platform turns into a GitHub
|
||||
issue on the platform repo (wired in Phase 25; the Lambda returns a
|
||||
prepared-status stub until then).
|
||||
|
||||
The Lambda handler and the Terraform that deploys it live in
|
||||
[`core/lambda/contract_ingestor.py`](https://github.com/acdl/acdl/blob/main/core/lambda/contract_ingestor.py)
|
||||
and
|
||||
[`terraform/platform/main.tf`](https://github.com/acdl/acdl/blob/main/terraform/platform/main.tf)
|
||||
respectively.
|
||||
|
||||
## Onboarding scaffold (current state)
|
||||
|
||||
The platform repo ships a minimal onboarding scaffold:
|
||||
|
||||
+12
-1
@@ -23,12 +23,15 @@ 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) |
|
||||
| `rds` | `aws_db_instance` — RDS database instance (multi-engine: postgres, mysql, etc.) | [modules/l1/rds/README.md](https://github.com/acdl/acdl/blob/main/modules/l1/rds/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
|
||||
@@ -37,6 +40,14 @@ Module versions are tracked in
|
||||
[`registry.json`](https://github.com/acdl/acdl/blob/main/modules/registry.json).
|
||||
Both primitives and modules are registered.
|
||||
|
||||
## Examples
|
||||
|
||||
Each module has a `examples/` directory containing validated consumer
|
||||
contract examples (`simple.yaml` + `complex.yaml` + variation files). The
|
||||
platform-test pipeline validates them against
|
||||
[`schemas/contract.schema.json`](https://github.com/acdl/acdl/blob/main/schemas/contract.schema.json).
|
||||
See each module's `## Examples` section for the excerpts.
|
||||
|
||||
## Versioning
|
||||
|
||||
Primitives and modules use semver: interface → MAJOR, behavior → MINOR,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,18 @@ Terraform resources this module creates:
|
||||
|------|------|-------------|
|
||||
| `<name>` | string | description |
|
||||
|
||||
## NFRs
|
||||
|
||||
Non-functional requirements declared by the module's interface. Every
|
||||
L1 primitive MUST declare `deletion_protection` and `encryption_enabled`
|
||||
(both boolean, default `true`); they are mandatory NFRs for every L1.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `deletion_protection` | boolean | true | Prevent resource destruction via Terraform lifecycle prevent_destroy. |
|
||||
| `encryption_enabled` | boolean | true | Enable encryption (at rest or in transit, as applicable). |
|
||||
| `<name>` | <type> | <default> | description |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
|
||||
+6
-1
@@ -31,13 +31,18 @@ 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) |
|
||||
| `rds` | `aws_db_instance` — Relational database (PostgreSQL, MySQL, etc.) with multi-engine support | [README](l1/rds/README.md) |
|
||||
| `kms-key` | `aws_kms_key` — Customer-managed KMS key with rotation enabled (per-stack CMK) | [README](l1/kms-key/README.md) |
|
||||
| `uptime` | `aws_ecs_service` — Uptime-kuma monitoring on ECS Fargate with alert channels | [README](l1/uptime/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
|
||||
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
# ACDL Module Engineering Standards
|
||||
|
||||
Standards for authoring and reviewing ACDL modules. These standards
|
||||
govern the two module tiers — **L1 primitives** (single cloud resource
|
||||
or small group of related resources) and **L2 modules** (compositions
|
||||
that reference L1 primitives to deploy a complete stack) — and the
|
||||
substrate adapter that compiles them to Terraform. They are written for
|
||||
**platform engineers** and **AI agents** that author or review new
|
||||
modules against the existing corpus (12 L1 primitives and 2 L2 modules
|
||||
shipped in v1.8).
|
||||
|
||||
A module that fails any section below is not ready to publish.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
These standards codify the conventions already established by the
|
||||
shipped modules (`s3`, `vpc`, `ecs-cluster`, `ecs-service`, `iam-role`,
|
||||
`alb`, `ecr`, `cloudfront`, `waf`, `rds`, `kms-key`, `uptime`; the L2
|
||||
modules `static-assets` and `microservice`). They exist so that:
|
||||
|
||||
- platform engineers can review a new module against a fixed checklist;
|
||||
- AI agents authoring modules produce code that passes review without
|
||||
iteration; and
|
||||
- the substrate adapter (`adapters/terraform/adapter.py`) can compile a
|
||||
module instance with no module-specific code in the adapter beyond the
|
||||
three tables in §8.
|
||||
|
||||
When this document and an existing module disagree, the existing module
|
||||
is the authority for v1.x. A change to this document is a MINOR version
|
||||
bump of the standards; a change that breaks shipped modules is a MAJOR
|
||||
bump and requires a migration plan.
|
||||
|
||||
## 2. L1 Primitive Standards
|
||||
|
||||
An L1 primitive is a single cloud resource or a small group of related
|
||||
resources (e.g. a VPC with subnets and a route table). It is declared by
|
||||
an `interface.json` and realized by the substrate adapter; it does not
|
||||
own Terraform code.
|
||||
|
||||
### 2.1 Required files
|
||||
|
||||
Every L1 primitive MUST contain, at minimum:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `interface.json` | Substrate-agnostic declaration: inputs, outputs, NFRs, optional multi-resource graph. |
|
||||
| `instance.json` | A concrete instance used as the adapter regression baseline. |
|
||||
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
||||
| `examples/simple.yaml` | A minimal contract that uses the primitive with required inputs only. |
|
||||
| `examples/complex.yaml` | A contract that exercises optional inputs, NFRs, and (if applicable) the multi-resource graph. |
|
||||
|
||||
Directory layout:
|
||||
|
||||
```
|
||||
modules/l1/<name>/
|
||||
interface.json
|
||||
instance.json
|
||||
README.md
|
||||
examples/
|
||||
simple.yaml
|
||||
complex.yaml
|
||||
```
|
||||
|
||||
### 2.2 interface.json schema
|
||||
|
||||
`interface.json` MUST be a JSON object with the following required
|
||||
fields:
|
||||
|
||||
| Field | Type | Constraint |
|
||||
|-------|------|------------|
|
||||
| `name` | string | `^[a-z][a-z0-9-]*$`; MUST match the module folder name. |
|
||||
| `version` | string | Semver (`^\d+\d+\.\d+$`); MUST match the registry entry semver. |
|
||||
| `kind` | string | Literal `"l1"`. |
|
||||
| `type` | string | Stack type in `aws:<service>:<kind>` format (see §2.7). |
|
||||
| `description` | string | One or two sentences in plain language; no Terraform jargon. |
|
||||
| `inputs` | object | Keyed by input name; each value is an input declaration (§2.3). MAY be empty. |
|
||||
| `outputs` | object | Keyed by output name; each value is an output declaration (§2.4). MAY be empty. |
|
||||
| `nfrs` | object | Keyed by NFR name; each value is an NFR declaration (§2.5). MUST include `deletion_protection` and `encryption_enabled`. |
|
||||
|
||||
Optional fields for multi-resource primitives:
|
||||
|
||||
| Field | Type | Constraint |
|
||||
|-------|------|------------|
|
||||
| `resources` | array | One entry per distinct cloud resource; see §2.6. |
|
||||
| `intra_refs` | array | Internal wiring between resources; see §2.6. |
|
||||
|
||||
A primitive that creates a single resource (e.g. `s3`, `iam-role`,
|
||||
`rds`, `kms-key`) omits `resources` and `intra_refs`; its `type` field
|
||||
is the single resource's stack type. A primitive that creates a small
|
||||
group of related resources (e.g. `vpc`, `alb`, `cloudfront`) declares
|
||||
`resources[]` with one entry per resource and `intra_refs[]` for the
|
||||
internal wiring; its `type` field is the *primary* resource's stack
|
||||
type.
|
||||
|
||||
### 2.3 Input declaration
|
||||
|
||||
Each entry in `inputs` is an object:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | string | yes | One of: `string`, `number`, `boolean`, `array`, `object`. |
|
||||
| `description` | string | yes | Plain language; no Terraform jargon. |
|
||||
| `required` | boolean | yes | `true` if the consumer MUST supply this input. |
|
||||
| `default` | (any) | no | Present only when `required` is `false`. MUST match the declared `type`. |
|
||||
| `enum` | array | no | Allowed values for `string`/`number` inputs (e.g. RDS `engine`). |
|
||||
|
||||
`region` is a required `string` input on every primitive that creates a
|
||||
regional resource. Global resources (e.g. CloudFront) still declare
|
||||
`region` because the provider region is used for child resources (the
|
||||
OAC in the `cloudfront` case).
|
||||
|
||||
Every primitive that holds at-rest data MUST declare an optional
|
||||
`kms_key_arn` input (`string`, `required: false`); see §4.
|
||||
|
||||
### 2.4 Output declaration
|
||||
|
||||
Each entry in `outputs` is an object:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | string | yes | `arn` for ARN outputs; `string` for all others. |
|
||||
| `description` | string | yes | Plain language. |
|
||||
|
||||
Use `arn` (not `string`) for any output that returns an AWS ARN — the
|
||||
adapter and policy engine key off the `arn` type to apply ARN-scoped
|
||||
rules.
|
||||
|
||||
### 2.5 NFR declaration
|
||||
|
||||
Each entry in `nfrs` is an object:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `type` | string | yes | One of: `string`, `number`, `boolean`. |
|
||||
| `description` | string | yes | Plain language. |
|
||||
| `default` | (any) | yes | MUST match the declared `type`. NFRs always have a default. |
|
||||
|
||||
Mandatory NFRs on every L1:
|
||||
|
||||
| NFR | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `deletion_protection` | boolean | `true` | See §5. |
|
||||
| `encryption_enabled` | boolean | `true` | See §4. |
|
||||
|
||||
A primitive for which an NFR does not conceptually apply (e.g. an IAM
|
||||
role has no at-rest data) still declares it with `default: true` and a
|
||||
description noting the non-applicability, so the standards check and the
|
||||
adapter emit logic stay uniform. The shipped `iam-role` primitive is the
|
||||
reference for this case.
|
||||
|
||||
Additional NFRs are encouraged where they carry operational meaning
|
||||
(e.g. `s3.versioning`, `rds.backup_retention_period`,
|
||||
`kms-key.enable_rotation`, `vpc.flow_logs_encrypted`). Name them in
|
||||
lowercase snake_case.
|
||||
|
||||
### 2.6 Multi-resource pattern
|
||||
|
||||
A primitive that creates more than one cloud resource (e.g. `vpc`
|
||||
creates `aws_vpc` + `aws_subnet` + `aws_route_table`; `alb` creates
|
||||
`aws_lb` + `aws_lb_target_group` + `aws_lb_listener`; `cloudfront`
|
||||
creates `aws_cloudfront_distribution` +
|
||||
`aws_cloudfront_origin_access_control`) declares a `resources` array.
|
||||
|
||||
Each `resources[]` entry:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `type` | string | The resource's stack type (`aws:<service>:<kind>`). |
|
||||
| `description` | string | Plain language. |
|
||||
| `inputs` | array | Names (strings) of inputs from the top-level `inputs` object that this resource consumes. |
|
||||
| `outputs` | array | Names (strings) of outputs from the top-level `outputs` object that this resource produces. |
|
||||
|
||||
The top-level `inputs`/`outputs` objects remain the single source of
|
||||
truth; `resources[].inputs` and `resources[].outputs` are arrays of
|
||||
*names* referencing those objects, not re-declarations.
|
||||
|
||||
`intra_refs[]` wires outputs of one resource to inputs of another
|
||||
within the same primitive. Each entry:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `from` | string | `<resource-type>.<output-name>` — the producing side. |
|
||||
| `to` | string | `<resource-type>.<input-name>` — the consuming side. |
|
||||
|
||||
Reference: `cloudfront/interface.json` declares an intra-ref from
|
||||
`aws:cloudfront:distribution.oac_id` to
|
||||
`aws:cloudfront:originaccesscontrol.oac_id`; `vpc/interface.json`
|
||||
declares intra-refs from the subnet and route table to the VPC's
|
||||
`vpc_id`.
|
||||
|
||||
### 2.7 Naming and stack types
|
||||
|
||||
- Module folder names and `interface.json` `name` values MUST match
|
||||
`^[a-z][a-z0-9-]*$` (lowercase, hyphenated, leading letter). Examples:
|
||||
`s3`, `ecs-cluster`, `kms-key`, `iam-role`, `uptime`.
|
||||
- Input and output names are lowercase snake_case.
|
||||
- Stack types follow `aws:<service>:<kind>`:
|
||||
- `aws:s3:bucket`
|
||||
- `aws:ec2:vpc`, `aws:ec2:subnet`, `aws:ec2:routetable`
|
||||
- `aws:ecs:cluster`, `aws:ecs:task_definition`, `aws:ecs:service`,
|
||||
`aws:ecs:uptime-service`
|
||||
- `aws:iam:role`
|
||||
- `aws:elbv2:loadbalancer`, `aws:elbv2:listener`,
|
||||
`aws:elbv2:targetgroup`
|
||||
- `aws:ecr:repository`
|
||||
- `aws:cloudfront:distribution`, `aws:cloudfront:originaccesscontrol`
|
||||
- `aws:wafv2:webacl`
|
||||
- `aws:rds:instance`
|
||||
- `aws:kms:key`, `aws:kms:alias`
|
||||
- The substrate adapter's `TYPE_MAP` is the registry of stack types the
|
||||
adapter can compile (see §8). A new stack type requires a `TYPE_MAP`
|
||||
entry before the primitive can be deployed.
|
||||
|
||||
## 3. L2 Module Standards
|
||||
|
||||
An L2 module is a composition that references one or more L1 primitives
|
||||
to deploy a complete stack (e.g. an ECS Fargate microservice, a static
|
||||
asset site behind CloudFront + WAF). It is declared by a
|
||||
`composition.json`; it does not own Terraform code and does not have an
|
||||
`instance.json`.
|
||||
|
||||
### 3.1 Required files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `composition.json` | The composition tree: children, wires, outputs, optional features. |
|
||||
| `README.md` | Plain-language documentation following `README-TEMPLATE.md` (see §7). |
|
||||
| `examples/simple.yaml` | A minimal contract that uses the module with required inputs only. |
|
||||
| `examples/complex.yaml` | A contract that exercises optional inputs and feature flags. |
|
||||
|
||||
Directory layout:
|
||||
|
||||
```
|
||||
modules/l2/<name>/
|
||||
composition.json
|
||||
README.md
|
||||
examples/
|
||||
simple.yaml
|
||||
complex.yaml
|
||||
```
|
||||
|
||||
There is no `instance.json` for an L2 module — the L2 is deployed by
|
||||
resolving the composition tree to L1 instances at compile time, not by
|
||||
loading a pre-baked instance.
|
||||
|
||||
### 3.2 composition.json schema
|
||||
|
||||
`composition.json` MUST be a JSON object with the following fields:
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `name` | string | yes | `^[a-z][a-z0-9-]*$`; matches the module folder name. |
|
||||
| `version` | string | yes | Semver; matches the registry entry. |
|
||||
| `kind` | string | yes | Literal `"l2"`. |
|
||||
| `depth` | integer | yes | Literal `1` in v1 (see §3.5). |
|
||||
| `description` | string | yes | Plain language. |
|
||||
| `children` | array | yes | One entry per referenced L1 module (§3.3). |
|
||||
| `wires` | array | yes | Wires from contract inputs / child outputs to child inputs / stack outputs (§3.4). |
|
||||
| `outputs` | array | yes | Wires from child outputs to stack outputs (§3.4). |
|
||||
| `features` | object | no | Feature flags propagated to children by the resolver (§3.6). |
|
||||
|
||||
### 3.3 Children
|
||||
|
||||
Each `children[]` entry:
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `id` | string | The child id, unique within the composition. `^[a-z][a-z0-9-]*$`. The id is the local name used in wires (e.g. `vpc`, `cluster`, `kms`). |
|
||||
| `module` | string | `<name>@<semver>` referencing a registered L1 module. |
|
||||
|
||||
Children MUST reference L1 modules registered in `registry.json` (see
|
||||
§6). The referenced semver MUST exist in the registry. An L2 MUST NOT
|
||||
reference another L2 (no L3 in v1; see §3.5).
|
||||
|
||||
Reference: `microservice/composition.json` declares seven children
|
||||
(`vpc`, `cluster`, `ecr`, `roles`, `alb`, `service`, `kms`), each
|
||||
referencing an L1 at `@1.0.0`.
|
||||
|
||||
### 3.4 Wire format
|
||||
|
||||
A wire is a JSON object `{"from": "<source>", "to": "<target>"}` with an
|
||||
optional `default` field for contract-input wires.
|
||||
|
||||
Sources (the `from` side):
|
||||
|
||||
| Source form | Meaning |
|
||||
|-------------|---------|
|
||||
| `contract.inputs.<name>` | A value supplied by the consumer's contract YAML. |
|
||||
| `<childId>.outputs.<name>` | An output produced by a child L1 module. |
|
||||
|
||||
Targets (the `to` side):
|
||||
|
||||
| Target form | Meaning |
|
||||
|-------------|---------|
|
||||
| `<childId>.inputs.<name>` | An input on a child L1 module. |
|
||||
| `stack.outputs.<name>` | A value the L2 exposes as a stack output. |
|
||||
|
||||
Wires that source from `contract.inputs.<name>` MAY carry a `default`
|
||||
value used when the consumer omits the input. Reference:
|
||||
`microservice/composition.json` wires `contract.inputs.bucket_name` to
|
||||
`vpc.inputs.cidr` with `default: "10.0.0.0/16"` (a historical quirk
|
||||
preserved for regression).
|
||||
|
||||
The `outputs[]` array uses the same wire shape but its `to` is always
|
||||
`stack.outputs.<name>` and its `from` is always
|
||||
`<childId>.outputs.<name>`.
|
||||
|
||||
### 3.5 Maximum depth
|
||||
|
||||
`depth` is `1` for every L2 in v1. The composition tree is strictly L2
|
||||
→ L1: an L2 may reference only L1 primitives, never another L2. There
|
||||
is no L3 in v1. The stack schema permits `depth` up to 5 for forward
|
||||
compatibility, but the v1 resolver and adapter only handle depth 1.
|
||||
|
||||
### 3.6 Feature flags
|
||||
|
||||
An L2 MAY declare a `features` object. Two flags are defined in v1:
|
||||
|
||||
| Flag | Type | Default | Effect |
|
||||
|------|------|---------|--------|
|
||||
| `deletion_protection` | boolean | `true` | When `true`, the resolver propagates `deletion_protection: true` to every child's NFRs. When `false`, children are deployed with `deletion_protection: false` (used by decommission; see §5). |
|
||||
| `uptime_enabled` | boolean | `true` | When `true`, the uptime monitoring L1 is deployed after the L2 module in a separate terraform state. When `false`, the uptime deployment is skipped. |
|
||||
|
||||
Feature flags are propagated to children by the resolver; the L2
|
||||
`composition.json` does not need to wire them explicitly as inputs. The
|
||||
resolver reads `features` and injects the corresponding NFR/input on
|
||||
each child.
|
||||
|
||||
## 4. Encryption by Default
|
||||
|
||||
Encryption is mandatory and on by default across the platform.
|
||||
|
||||
1. Every L1 MUST declare an `encryption_enabled` NFR (boolean, default
|
||||
`true`) in `interface.json`. See §2.5.
|
||||
2. Every L1 that holds at-rest data (S3, RDS, ECR, ECS task
|
||||
definition env, VPC flow logs, CloudWatch log groups) MUST declare an
|
||||
optional `kms_key_arn` input (`string`, `required: false`). When
|
||||
supplied, the adapter wires it to the resource's KMS encryption
|
||||
argument.
|
||||
3. L2 modules MUST wire a per-stack customer-managed KMS key to all
|
||||
children that accept `kms_key_arn`. The KMS key is a `kms-key` child
|
||||
of the L2 — one key per L2 deployment, no shared keys. Reference:
|
||||
both `static-assets` and `microservice` declare a `kms` child
|
||||
(`kms-key@1.0.0`) and wire `kms.outputs.kms_key_arn` to every child
|
||||
that accepts a CMK.
|
||||
4. For a standalone L1 deployment (an L1 used outside an L2), if the
|
||||
consumer does not supply `kms_key_arn`, the adapter falls back to the
|
||||
AWS-managed default key for that service and emits a warning to
|
||||
stderr. The primitive is still encrypted; only the key manager
|
||||
differs.
|
||||
5. The `kms-key` primitive enables key rotation by default
|
||||
(`enable_rotation` NFR, default `true`), and the adapter emits
|
||||
`enable_key_rotation = true` on the `aws_kms_key` resource.
|
||||
|
||||
A primitive that does not hold at-rest data (e.g. `iam-role`,
|
||||
`ecs-cluster`, `alb`) still declares `encryption_enabled` for standards
|
||||
uniformity (see §2.5) but does not declare `kms_key_arn`.
|
||||
|
||||
## 5. Deletion Protection by Default
|
||||
|
||||
Deletion protection is mandatory and on by default to prevent
|
||||
accidental teardown of production infrastructure.
|
||||
|
||||
1. Every L1 MUST declare a `deletion_protection` NFR (boolean, default
|
||||
`true`) in `interface.json`. See §2.5.
|
||||
2. When `deletion_protection` is `true`, the substrate adapter emits a
|
||||
`lifecycle { prevent_destroy = true }` block on the corresponding
|
||||
Terraform resource. A `terraform destroy` against a protected
|
||||
resource fails with an error naming the resource.
|
||||
3. L2 modules expose `features.deletion_protection` (default `true`).
|
||||
The resolver propagates the flag to every child's NFRs (see §3.6).
|
||||
4. **Decommission mode.** To tear down a stack that was deployed with
|
||||
deletion protection, the consumer sets `inputs.deletion_protection:
|
||||
false` on the contract (or `features.deletion_protection: false` on
|
||||
an L2) and re-applies. The decommission transform
|
||||
(`decommission_transform`) zeroes capacity counts (e.g. ECS desired
|
||||
count to 0, RDS allocated storage to the minimum) so that the
|
||||
subsequent `destroy` applies against a quiesced stack. The transform
|
||||
is applied by the resolver before the adapter emits resources.
|
||||
|
||||
## 6. Registry
|
||||
|
||||
Every module — L1 and L2 — MUST be registered in
|
||||
`modules/registry.json` at its semver. The registry is the source of
|
||||
truth for what is published; the adapter and resolver refuse to compile
|
||||
a module that is not registered.
|
||||
|
||||
Registry entry shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"<module-name>": {
|
||||
"<semver>": {
|
||||
"interface": "modules/<l1|l2>/<module-name>/<interface.json|composition.json>",
|
||||
"published_at": "<ISO 8601 timestamp>",
|
||||
"deprecated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `interface` is the path (relative to the repo root) to the module's
|
||||
interface file — `interface.json` for an L1, `composition.json` for
|
||||
an L2.
|
||||
- `published_at` is an ISO 8601 timestamp. Use a full
|
||||
`YYYY-MM-DDTHH:MM:SSZ` form; do not omit the seconds or the timezone
|
||||
designator.
|
||||
- `deprecated` is `false` for a live module. A MAJOR version bump does
|
||||
not delete the old entry; it flips `deprecated` to `true` and starts a
|
||||
12-month deprecation window (see §7 Versioning).
|
||||
|
||||
A new semver of an existing module is a new key under the module's
|
||||
object; old semvers are retained. The registry is append-only for
|
||||
published semvers — a published semver is never edited or deleted.
|
||||
|
||||
## 7. README Standards
|
||||
|
||||
Every module README MUST follow the structure of
|
||||
`modules/README-TEMPLATE.md`. Required sections, in order:
|
||||
|
||||
1. `# <name> — <plain-language description>` — title with the module
|
||||
name and a one-line description.
|
||||
2. `## Overview` — one or two sentences in plain language.
|
||||
3. `## Resources` — a table of the Terraform resources the module
|
||||
creates (L1) or the primitives it references (L2).
|
||||
4. `## Inputs` — a table: `| Name | Type | Required | Default | Description |`.
|
||||
5. `## Outputs` — a table: `| Name | Type | Description |`.
|
||||
6. `## NFRs` — a table: `| Name | Type | Default | Description |`.
|
||||
`deletion_protection` and `encryption_enabled` are mandatory NFRs
|
||||
for every L1; they MUST appear in this table.
|
||||
7. `## Usage` — a concrete snippet showing how a consumer references
|
||||
the module in a contract.
|
||||
8. `## Compliance extension points` — resources or behaviors that could
|
||||
be added for the future compliance milestone (GDPR, SOX, SOC2, HIPAA,
|
||||
DORA). Not implemented yet; listed so the redesign can plan for them.
|
||||
9. `## Examples` — links to `examples/simple.yaml` and
|
||||
`examples/complex.yaml` with a one-line description of each.
|
||||
10. `## Versioning` — the module's semver policy: interface MAJOR,
|
||||
behavior MINOR, lifecycle PATCH. MAJOR bumps require a new
|
||||
`registry.json` entry (immutable publication); old entries enter a
|
||||
12-month deprecation window.
|
||||
|
||||
An L2 README's `## Resources` section lists the referenced L1 children
|
||||
rather than Terraform resources, and its `## Inputs`/`## Outputs`
|
||||
sections reflect the contract inputs and stack outputs of the
|
||||
composition.
|
||||
|
||||
## 8. Adapter Extension Pattern
|
||||
|
||||
The Terraform adapter (`adapters/terraform/adapter.py`) is a thin
|
||||
translator. It owns no module content; it only maps stack types and
|
||||
names to Terraform types and arguments via three tables and, for
|
||||
complex resources, a specialized emit branch.
|
||||
|
||||
### 8.1 The three tables
|
||||
|
||||
| Table | Purpose | Keys | Values |
|
||||
|-------|---------|------|--------|
|
||||
| `TYPE_MAP` | Stack type → Terraform resource type. | Stack type string (`aws:<service>:<kind>`). | Terraform resource type (`aws_s3_bucket`, `aws_db_instance`, etc.). |
|
||||
| `INPUT_MAP` | Stack input name → Terraform argument name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | Stack type. | Object mapping input name → Terraform arg name. |
|
||||
| `OUTPUT_MAP` | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings are listed. | Stack type. | Object mapping output name → Terraform attribute name. |
|
||||
|
||||
Reference: `adapter.py:26` (`TYPE_MAP`), `adapter.py:51` (`INPUT_MAP`),
|
||||
`adapter.py:75` (`OUTPUT_MAP`).
|
||||
|
||||
### 8.2 Specialized `_emit_resource` branches
|
||||
|
||||
Most resources emit with the generic loop in `_emit_resource`
|
||||
(`adapter.py:156`): for each input, look up the Terraform arg in
|
||||
`INPUT_MAP`, render the value, append `arg = value`. Resources with
|
||||
nested HCL blocks need a specialized branch. The shipped examples:
|
||||
|
||||
- `aws:ecs:service` emits a `load_balancer {}` block from the
|
||||
`lb_target_group_arn` input.
|
||||
- `aws:elbv2:loadbalancer` wraps `subnets` and `security_group` in list
|
||||
brackets.
|
||||
- `aws:cloudfront:distribution` emits nested `origin {}`,
|
||||
`default_cache_behavior {}`, and
|
||||
`server_side_encryption_configuration {}` blocks.
|
||||
- `aws:wafv2:webacl` emits nested `rules {}` blocks.
|
||||
- `aws:ecs:task_definition` emits a `container_definitions` jsonencode
|
||||
block from `image`/`port`/`env`.
|
||||
|
||||
A specialized branch lives inside `_emit_resource` and is keyed on the
|
||||
stack type. It reads the input value, renders the nested block, and
|
||||
appends the lines to `body`.
|
||||
|
||||
### 8.3 Adding a new L1 to the adapter
|
||||
|
||||
When a new L1 primitive is added:
|
||||
|
||||
1. Add one entry to `TYPE_MAP` for each stack type the primitive
|
||||
declares (single resource → one entry; multi-resource → one entry
|
||||
per resource in `resources[]`).
|
||||
2. Add one entry to `INPUT_MAP` for each stack type, listing only the
|
||||
inputs whose Terraform arg name differs from the stack input name
|
||||
(identity mappings are omitted).
|
||||
3. Add one entry to `OUTPUT_MAP` for each stack type, listing only the
|
||||
outputs whose Terraform attribute name differs from the stack output
|
||||
name.
|
||||
4. If any resource requires nested HCL blocks, add a specialized branch
|
||||
in `_emit_resource` keyed on that stack type.
|
||||
|
||||
If steps 1–3 are done and no specialized branch is needed, the
|
||||
primitive deploys with no further adapter changes. The L1 content and
|
||||
the contract YAML do not change when the adapter grows.
|
||||
|
||||
## 9. Code Review Checklist
|
||||
|
||||
Use this checklist when reviewing a new module (L1 or L2). Every box
|
||||
must be checked before the module is registered and published.
|
||||
|
||||
### 9.1 Files and structure
|
||||
|
||||
- [ ] All required files present:
|
||||
- L1: `interface.json`, `instance.json`, `README.md`,
|
||||
`examples/simple.yaml`, `examples/complex.yaml`.
|
||||
- L2: `composition.json`, `README.md`, `examples/simple.yaml`,
|
||||
`examples/complex.yaml` (no `instance.json`).
|
||||
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
|
||||
`schemas/stack.schema.json`.
|
||||
- [ ] `examples/simple.yaml` and `examples/complex.yaml` validate
|
||||
against `schemas/contract.schema.json`.
|
||||
- [ ] Module registered in `modules/registry.json` at its semver with a
|
||||
full ISO 8601 `published_at` and `deprecated: false`.
|
||||
|
||||
### 9.2 Interface (L1)
|
||||
|
||||
- [ ] `name` matches the folder name and `^[a-z][a-z0-9-]*$`.
|
||||
- [ ] `version` is semver and matches the registry entry.
|
||||
- [ ] `kind` is `"l1"`.
|
||||
- [ ] `type` follows `aws:<service>:<kind>`.
|
||||
- [ ] Every input has `type`, `description`, `required`; optional inputs
|
||||
carry a `default` of the correct type; `enum` present where the value
|
||||
set is constrained.
|
||||
- [ ] Every output has `type` (`arn` for ARNs, `string` otherwise) and
|
||||
`description`.
|
||||
- [ ] `nfrs` includes `deletion_protection` (boolean, default `true`)
|
||||
and `encryption_enabled` (boolean, default `true`).
|
||||
- [ ] `kms_key_arn` input present if the primitive holds at-rest data.
|
||||
- [ ] Multi-resource primitives declare `resources[]` (with `inputs`/
|
||||
`outputs` as arrays of names) and `intra_refs[]` with `{from, to}`.
|
||||
|
||||
### 9.3 Composition (L2)
|
||||
|
||||
- [ ] `kind` is `"l2"` and `depth` is `1`.
|
||||
- [ ] Every `children[]` entry is `{id, module}` with `module` in
|
||||
`<name>@<semver>` form referencing a registered L1.
|
||||
- [ ] No child references an L2 (no L3 in v1).
|
||||
- [ ] `wires[]` use the `contract.inputs.<name>` /
|
||||
`<childId>.outputs.<name>` → `<childId>.inputs.<name>` /
|
||||
`stack.outputs.<name>` forms.
|
||||
- [ ] `outputs[]` use `<childId>.outputs.<name>` →
|
||||
`stack.outputs.<name>`.
|
||||
- [ ] A `kms` child (`kms-key@<semver>`) is present and its
|
||||
`kms_key_arn` output is wired to every child that accepts a CMK.
|
||||
- [ ] `features` (if present) only uses defined flags
|
||||
(`deletion_protection`, `uptime_enabled`).
|
||||
|
||||
### 9.4 Adapter
|
||||
|
||||
- [ ] `TYPE_MAP` has an entry for every stack type the new primitive
|
||||
declares.
|
||||
- [ ] `INPUT_MAP` and `OUTPUT_MAP` have entries for every stack type,
|
||||
listing only non-identity mappings.
|
||||
- [ ] A specialized `_emit_resource` branch is added for any resource
|
||||
that needs nested HCL blocks.
|
||||
- [ ] The new primitive's `instance.json` round-trips through the
|
||||
adapter without error (regression baseline).
|
||||
|
||||
### 9.5 README and docs
|
||||
|
||||
- [ ] README follows `README-TEMPLATE.md` with all required sections in
|
||||
order (§7).
|
||||
- [ ] `## NFRs` table lists `deletion_protection` and
|
||||
`encryption_enabled` for an L1.
|
||||
- [ ] `## Compliance extension points` lists at least one plausible
|
||||
future extension.
|
||||
|
||||
### 9.6 Tests
|
||||
|
||||
- [ ] A test is added for the new primitive covering adapter emission
|
||||
(the Terraform output for `instance.json` matches the expected
|
||||
fixture) and interface validation (`interface.json` validates against
|
||||
`stack.schema.json`).
|
||||
- [ ] For an L2, a test is added that the composition resolves to the
|
||||
expected set of L1 instances and that the adapter emits a root module
|
||||
calling the L1 modules.
|
||||
@@ -64,6 +64,48 @@ The `target_group_arn` output is referenced by `ecs-service` as its
|
||||
- **WAF** — add `aws_wafv2_web_acl_association` for application-layer protection (SOC2 CC7.6, PCI-DSS 6.5, DORA ICT risk).
|
||||
- **Deregistration delay** — add `deregistration_delay` for graceful draining (SOC2 CC9.1 resilience).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: alb
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-alb
|
||||
subnets: subnet-aaa,subnet-bbb
|
||||
security_group: sg-xxx
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex ALB with HTTPS + ACM cert (requires a consumer-supplied domain)
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: alb
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-alb
|
||||
subnets: subnet-aaa,subnet-bbb
|
||||
security_group: sg-xxx
|
||||
port: 443
|
||||
protocol: HTTPS
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Complex ALB with HTTPS + ACM cert (requires a consumer-supplied domain)
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: alb
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-alb
|
||||
subnets: subnet-aaa,subnet-bbb
|
||||
security_group: sg-xxx
|
||||
port: 443
|
||||
protocol: HTTPS
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,10 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: alb
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-alb
|
||||
subnets: subnet-aaa,subnet-bbb
|
||||
security_group: sg-xxx
|
||||
port: 80
|
||||
protocol: HTTP
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "alb",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "alb-loadbalancer",
|
||||
"type": "aws:elbv2:loadbalancer",
|
||||
"module": "alb@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-alb",
|
||||
"subnets": "subnet-12345",
|
||||
"security_group": "sg-12345",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"lb_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "alb-targetgroup",
|
||||
"type": "aws:elbv2:targetgroup",
|
||||
"module": "alb@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-alb",
|
||||
"port": 80,
|
||||
"protocol": "HTTP",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"target_group_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "alb-listener",
|
||||
"type": "aws:elbv2:listener",
|
||||
"module": "alb@1.0.0",
|
||||
"inputs": {
|
||||
"port": 80,
|
||||
"protocol": "HTTP",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"listener_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -52,7 +52,23 @@
|
||||
"description": "The target group ARN."
|
||||
}
|
||||
},
|
||||
"nfrs": {},
|
||||
"nfrs": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable TLS/HTTPS encryption in transit.",
|
||||
"default": true
|
||||
},
|
||||
"tls_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable TLS listener.",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:elbv2:loadbalancer",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# 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).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
# Simple CloudFront distribution (S3 origin, no WAF)
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: cloudfront
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex CloudFront with WAF + custom TTL + viewer protocol redirect
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: cloudfront
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||
price_class: PriceClass_100
|
||||
viewer_protocol_policy: redirect-to-https
|
||||
default_ttl: 3600
|
||||
max_ttl: 86400
|
||||
waf_web_acl_arn: arn:aws:wafv2:us-east-1:000000000000:webacl/my-waf
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Complex CloudFront with WAF + custom TTL + viewer protocol redirect
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: cloudfront
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||
price_class: PriceClass_100
|
||||
viewer_protocol_policy: redirect-to-https
|
||||
default_ttl: 3600
|
||||
max_ttl: 86400
|
||||
waf_web_acl_arn: arn:aws:wafv2:us-east-1:000000000000:webacl/my-waf
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,7 @@
|
||||
# Simple CloudFront distribution (S3 origin, no WAF)
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: cloudfront
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_regional_domain_name: my-bucket.s3.us-east-1.amazonaws.com
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "cloudfront",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "cloudfront-originaccesscontrol",
|
||||
"type": "aws:cloudfront:originaccesscontrol",
|
||||
"module": "cloudfront@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-oac",
|
||||
"origin_type": "s3",
|
||||
"signing_behavior": "always",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"oac_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cloudfront-distribution",
|
||||
"type": "aws:cloudfront:distribution",
|
||||
"module": "cloudfront@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_regional_domain_name": "acdl-spike-bucket.s3.us-east-1.amazonaws.com",
|
||||
"price_class": "PriceClass_100",
|
||||
"viewer_protocol_policy": "redirect-to-https",
|
||||
"default_ttl": 3600,
|
||||
"max_ttl": 86400,
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"distribution_arn": {
|
||||
"type": "arn"
|
||||
},
|
||||
"distribution_domain_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"oac_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"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": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable encryption in transit (HTTPS only).",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
@@ -51,6 +51,40 @@ The `repository_url` output is used to build the `image` input for
|
||||
- **Lifecycle policy** — add `aws_ecr_lifecycle_policy` to enforce image retention / cleanup (GDPR Art.5(2) data minimization, SOC2 CC5.2).
|
||||
- **Access policy** — add a repository policy restricting pull/push to known roles (SOC2 CC6.1, HIPAA §164.308(a)(4)).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecr
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-repo
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex ECR with lifecycle policy + image scanning
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecr
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-repo
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Complex ECR with lifecycle policy + image scanning
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecr
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-repo
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecr
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-repo
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "ecr",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "ecr",
|
||||
"type": "aws:ecr:repository",
|
||||
"module": "ecr@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-demo",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"repository_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"repository_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
"type": "string",
|
||||
"description": "AWS region the repository is created in.",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for repository encryption; if absent, uses AWS-managed key.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -26,5 +31,21 @@
|
||||
"description": "The ECR repository ARN."
|
||||
}
|
||||
},
|
||||
"nfrs": {}
|
||||
"nfrs": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable repository encryption (KMS).",
|
||||
"default": true
|
||||
},
|
||||
"encryption_type": {
|
||||
"type": "string",
|
||||
"description": "Encryption type.",
|
||||
"default": "KMS"
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,40 @@ The `cluster_arn` output is referenced by `ecs-service` as its
|
||||
- **CloudWatch Logs** — add a log group with retention policy for cluster-level audit logs (SOX, SOC2 CC7.2, HIPAA §164.312(b)).
|
||||
- **Encryption** — add `settings { name = "containerInsights", value = "enabled" }` and KMS-based encryption for container data (HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-cluster
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-cluster
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex ECS cluster with container insights
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-cluster
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-cluster
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Complex ECS cluster with container insights
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-cluster
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-cluster
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-cluster
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-cluster
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "ecs-cluster",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "ecs-cluster",
|
||||
"type": "aws:ecs:cluster",
|
||||
"module": "ecs-cluster@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-cluster",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"cluster_arn": {
|
||||
"type": "arn"
|
||||
},
|
||||
"cluster_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
"type": "string",
|
||||
"description": "AWS region the cluster is created in.",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -26,5 +31,16 @@
|
||||
"description": "The ECS cluster id (name)."
|
||||
}
|
||||
},
|
||||
"nfrs": {}
|
||||
"nfrs": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable CloudWatch log group encryption.",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,47 @@ provided.
|
||||
- **Deployment circuit breaker** — add `deployment_circuit_breaker` block for resilience (SOC2 CC9.1, DORA operational resilience).
|
||||
- **Health check** — add a `health_check` block to the target group (currently missing despite the contract schema having a healthcheck field).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-service
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-service
|
||||
region: us-east-1
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex ECS service with env vars + health check
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-service
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-service
|
||||
region: us-east-1
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 8080
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
ENVIRONMENT: production
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Complex ECS service with env vars + health check
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-service
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-service
|
||||
region: us-east-1
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 8080
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
ENVIRONMENT: production
|
||||
@@ -0,0 +1,8 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: ecs-service
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-service
|
||||
region: us-east-1
|
||||
image: public.ecr.aws/docker/library/nginx:latest
|
||||
port: 80
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "ecs-service",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "service-taskdefinition",
|
||||
"type": "aws:ecs:task_definition",
|
||||
"module": "ecs-service@1.0.0",
|
||||
"inputs": {
|
||||
"image": "public.ecr.aws/docker/library/nginx:latest",
|
||||
"port": 80,
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"task_def_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service-service",
|
||||
"type": "aws:ecs:service",
|
||||
"module": "ecs-service@1.0.0",
|
||||
"inputs": {
|
||||
"cluster_arn": "arn:aws:ecs:us-east-1:123456789012:cluster/acdl-cluster",
|
||||
"subnets": "subnet-12345",
|
||||
"security_group": "sg-12345",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"service_arn": {
|
||||
"type": "arn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -56,6 +56,11 @@
|
||||
"type": "string",
|
||||
"description": "AWS region the service is created in.",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -68,7 +73,18 @@
|
||||
"description": "The ECS task definition ARN."
|
||||
}
|
||||
},
|
||||
"nfrs": {},
|
||||
"nfrs": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable CloudWatch log group encryption.",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "aws:ecs:task_definition",
|
||||
|
||||
@@ -57,6 +57,40 @@ into the Terraform `assume_role_policy` argument. The
|
||||
- **Access Analyzer** — add `aws_accessanalyzer_analyzer` to verify least-privilege (SOC2 CC6.1, GDPR Art.32).
|
||||
- **Role separation** — add a separate task role vs. execution role (SOC2 CC6.3 segregation of duties).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: iam-role
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-task-role
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex IAM role with managed policies
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: iam-role
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-task-role
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Complex IAM role with managed policies
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: iam-role
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-production-task-role
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: iam-role
|
||||
environment: dev
|
||||
inputs:
|
||||
name: my-task-role
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "iam-role",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "iam-role",
|
||||
"type": "aws:iam:role",
|
||||
"module": "iam-role@1.0.0",
|
||||
"inputs": {
|
||||
"role_name": "acdl-task-role",
|
||||
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"role_arn": {
|
||||
"type": "arn"
|
||||
},
|
||||
"role_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -36,5 +36,16 @@
|
||||
"description": "The IAM role id."
|
||||
}
|
||||
},
|
||||
"nfrs": {}
|
||||
"nfrs": {
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Encryption is not applicable to IAM roles but included for standards compliance.",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# kms-key — KMS customer-managed key
|
||||
|
||||
> **Module kind:** primitive | **Version:** 1.0.0
|
||||
|
||||
A customer-managed KMS key for per-stack encryption. Created with key
|
||||
rotation enabled. One key per L2 deployment (no shared keys).
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Type | Purpose |
|
||||
|----------|------|---------|
|
||||
| kms-key | `aws_kms_key` | The KMS customer-managed key |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `description` | string | yes | — | Description of the KMS key |
|
||||
| `region` | string | yes | — | AWS region the KMS key is created in |
|
||||
| `deletion_window_days` | number | no | 30 | Number of days before the key is deleted after deletion is requested |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `kms_key_arn` | arn | The ARN of the KMS key |
|
||||
| `kms_key_id` | string | The ID of the KMS key |
|
||||
|
||||
## NFRs
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `enable_rotation` | boolean | true | Enable automatic key rotation |
|
||||
| `deletion_protection` | boolean | true | Prevent key destruction |
|
||||
| `encryption_enabled` | boolean | true | Encryption is always enabled for a KMS key |
|
||||
|
||||
## Usage
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "kms-key",
|
||||
"type": "aws:kms:key",
|
||||
"module": "kms-key@1.0.0",
|
||||
"inputs": {
|
||||
"description": "ACDL per-stack CMK",
|
||||
"region": "us-east-1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A concrete instance is at `instance.json` (used by the platform
|
||||
pipeline as the regression baseline).
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
- **Key rotation** — automatic key rotation enabled by default (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||
- **Deletion protection** — pending deletion window prevents accidental destruction (SOC2 CC7.2).
|
||||
- **Key policy** — restrict key usage to the stack's IAM roles (SOC2 CC6.1, GDPR Art.32).
|
||||
- **Audit logging** — CloudTrail logs all KMS API calls (SOC2 CC7.2, DORA audit trail).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: kms-key
|
||||
environment: dev
|
||||
inputs:
|
||||
description: "Simple CMK for testing"
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: kms-key
|
||||
environment: dev
|
||||
inputs:
|
||||
description: "Production CMK with 90-day deletion window"
|
||||
region: us-east-1
|
||||
deletion_window_days: 90
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: kms-key
|
||||
environment: dev
|
||||
inputs:
|
||||
description: "Production CMK with 90-day deletion window"
|
||||
region: us-east-1
|
||||
deletion_window_days: 90
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: kms-key
|
||||
environment: dev
|
||||
inputs:
|
||||
description: "Simple CMK for testing"
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "kms-key",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "kms-key",
|
||||
"type": "aws:kms:key",
|
||||
"module": "kms-key@1.0.0",
|
||||
"inputs": {
|
||||
"description": "ACDL per-stack CMK",
|
||||
"region": "us-east-1",
|
||||
"deletion_window_days": 30
|
||||
},
|
||||
"outputs": {
|
||||
"kms_key_arn": {
|
||||
"type": "arn"
|
||||
},
|
||||
"kms_key_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"nfrs": {
|
||||
"enable_rotation": true,
|
||||
"deletion_protection": true,
|
||||
"encryption_enabled": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "kms-key",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:kms:key",
|
||||
"description": "A customer-managed KMS key for per-stack encryption. Created with key rotation enabled. One key per L2 deployment (no shared keys).",
|
||||
"inputs": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the KMS key.",
|
||||
"required": true
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "AWS region the KMS key is created in.",
|
||||
"required": true
|
||||
},
|
||||
"deletion_window_days": {
|
||||
"type": "number",
|
||||
"description": "Number of days before the key is deleted after deletion is requested (default 30).",
|
||||
"required": false,
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"kms_key_arn": {
|
||||
"type": "arn",
|
||||
"description": "The ARN of the KMS key."
|
||||
},
|
||||
"kms_key_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the KMS key."
|
||||
}
|
||||
},
|
||||
"nfrs": {
|
||||
"enable_rotation": {
|
||||
"type": "boolean",
|
||||
"description": "Enable automatic key rotation.",
|
||||
"default": true
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent key destruction.",
|
||||
"default": true
|
||||
},
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Encryption is always enabled for a KMS key.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
# rds — RDS database instance
|
||||
|
||||
> **Module kind:** primitive | **Version:** 1.0.0
|
||||
|
||||
An RDS database instance. Supports multiple database engines (postgres,
|
||||
mysql, mariadb, sqlserver, oracle) via the `engine` input. The adapter
|
||||
translates the substrate-agnostic `aws:rds:instance` stack type to the
|
||||
Terraform `aws_db_instance` resource.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Type | Purpose |
|
||||
|----------|------|---------|
|
||||
| `rds` | `aws_db_instance` | The RDS database instance |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `engine` | string | yes | — | Database engine (postgres, mysql, mariadb, sqlserver-ex, sqlserver-web, sqlserver-se, oracle-se2) |
|
||||
| `engine_version` | string | yes | — | Engine version (e.g. 16.4 for postgres, 8.4 for mysql) |
|
||||
| `instance_class` | string | yes | `db.t3.micro` | RDS instance class (e.g. db.t3.micro, db.r6g.large) |
|
||||
| `allocated_storage` | number | no | 20 | Allocated storage in GB |
|
||||
| `db_name` | string | yes | — | The database name (some engines have restrictions) |
|
||||
| `username` | string | yes | — | Master username |
|
||||
| `multi_az` | boolean | no | false | Multi-AZ deployment |
|
||||
| `storage_encrypted` | boolean | no | true | Enable storage encryption |
|
||||
| `region` | string | yes | — | AWS region the RDS instance is created in |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `db_endpoint` | string | The RDS instance endpoint (host:port) |
|
||||
| `db_arn` | arn | The RDS instance ARN |
|
||||
|
||||
## NFRs
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `backup_retention_period` | number | 7 | Backup retention period in days |
|
||||
| `deletion_protection` | boolean | true | Enable deletion protection (default true for prod) |
|
||||
|
||||
## Usage
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "rds",
|
||||
"type": "aws:rds:instance",
|
||||
"module": "rds@1.0.0",
|
||||
"inputs": {
|
||||
"engine": "postgres",
|
||||
"engine_version": "16.4",
|
||||
"instance_class": "db.t3.micro",
|
||||
"allocated_storage": 20,
|
||||
"db_name": "acdl_db",
|
||||
"username": "acdl_admin",
|
||||
"multi_az": false,
|
||||
"storage_encrypted": true,
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"nfrs": {
|
||||
"backup_retention_period": 7,
|
||||
"deletion_protection": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A concrete instance is at `instance.json` (used by the platform pipeline
|
||||
as the regression baseline).
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
- **KMS encryption** — add a customer-managed KMS key for storage
|
||||
encryption (`kms_key_id` argument) (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv),
|
||||
GDPR Art.32).
|
||||
- **Automated backups** — the `backup_retention_period` NFR controls
|
||||
automated backup retention; extend with backup windows + copy tags to
|
||||
another region for DR (SOX ITGC, DORA operational resilience).
|
||||
- **Audit logging via CloudTrail** — RDS does not emit CloudTrail events
|
||||
for data-plane access; add `aws_db_instance_automated_backups_replication`
|
||||
+ CloudWatch Logs for database audit (SOX, SOC2 CC7.2, HIPAA
|
||||
§164.312(b)).
|
||||
- **IAM database authentication** — add `iam_database_authentication_enabled
|
||||
= true` so IAM users/roles can authenticate to the database without
|
||||
long-lived passwords (SOC2 CC6.1, HIPAA §164.308(a)(4)).
|
||||
- **Read replicas** — add `aws_db_instance` with `replicate_source_db` for
|
||||
read scaling and DR failover (SOC2 CC9.1, DORA operational resilience).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
# Simple RDS postgres instance
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: postgres
|
||||
engine_version: "16.4"
|
||||
instance_class: db.t3.micro
|
||||
allocated_storage: 20
|
||||
db_name: my_app_db
|
||||
username: db_admin
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
# Complex RDS postgres with multi-AZ + encryption
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: postgres
|
||||
engine_version: "16.4"
|
||||
instance_class: db.r6g.large
|
||||
allocated_storage: 100
|
||||
db_name: my_production_db
|
||||
username: db_admin
|
||||
multi_az: true
|
||||
storage_encrypted: true
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Multi-engine variation
|
||||
|
||||
RDS supports multiple database engines. The `engine` input selects the engine;
|
||||
the `engine_version` must match.
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml) — postgres 16.4
|
||||
|
||||
#### MySQL
|
||||
|
||||
[`examples/mysql.yaml`](examples/mysql.yaml) — mysql 8.4
|
||||
|
||||
```yaml
|
||||
# RDS mysql variation
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: mysql
|
||||
engine_version: "8.4"
|
||||
instance_class: db.t3.micro
|
||||
allocated_storage: 20
|
||||
db_name: my_mysql_db
|
||||
username: db_admin
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Complex RDS postgres with multi-AZ + encryption
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: postgres
|
||||
engine_version: "16.4"
|
||||
instance_class: db.r6g.large
|
||||
allocated_storage: 100
|
||||
db_name: my_production_db
|
||||
username: db_admin
|
||||
multi_az: true
|
||||
storage_encrypted: true
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,12 @@
|
||||
# RDS mysql variation
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: mysql
|
||||
engine_version: "8.4"
|
||||
instance_class: db.t3.micro
|
||||
allocated_storage: 20
|
||||
db_name: my_mysql_db
|
||||
username: db_admin
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,12 @@
|
||||
# Simple RDS postgres instance
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: rds
|
||||
environment: dev
|
||||
inputs:
|
||||
engine: postgres
|
||||
engine_version: "16.4"
|
||||
instance_class: db.t3.micro
|
||||
allocated_storage: 20
|
||||
db_name: my_app_db
|
||||
username: db_admin
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "rds",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": "rds",
|
||||
"type": "aws:rds:instance",
|
||||
"module": "rds@1.0.0",
|
||||
"inputs": {
|
||||
"engine": "postgres",
|
||||
"engine_version": "16.4",
|
||||
"instance_class": "db.t3.micro",
|
||||
"allocated_storage": 20,
|
||||
"db_name": "acdl_db",
|
||||
"username": "acdl_admin",
|
||||
"multi_az": false,
|
||||
"storage_encrypted": true,
|
||||
"region": "us-east-1"
|
||||
},
|
||||
"outputs": {
|
||||
"db_endpoint": {"type": "string", "description": "The RDS instance endpoint."},
|
||||
"db_arn": {"type": "arn", "description": "The RDS instance ARN."}
|
||||
},
|
||||
"nfrs": {
|
||||
"backup_retention_period": 7,
|
||||
"deletion_protection": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"name": "rds",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:rds:instance",
|
||||
"description": "RDS database instance primitive (substrate-agnostic stack type aws:rds:instance; the Terraform adapter translates to aws_db_instance). Supports multiple engines (postgres, mysql, etc.) via the engine input.",
|
||||
"inputs": {
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"description": "Database engine (postgres, mysql, mariadb, sqlserver, oracle).",
|
||||
"required": true,
|
||||
"enum": ["postgres", "mysql", "mariadb", "sqlserver-ex", "sqlserver-web", "sqlserver-se", "oracle-se2"]
|
||||
},
|
||||
"engine_version": {
|
||||
"type": "string",
|
||||
"description": "Engine version (e.g. 16.4 for postgres, 8.4 for mysql).",
|
||||
"required": true
|
||||
},
|
||||
"instance_class": {
|
||||
"type": "string",
|
||||
"description": "RDS instance class (e.g. db.t3.micro, db.r6g.large).",
|
||||
"required": true,
|
||||
"default": "db.t3.micro"
|
||||
},
|
||||
"allocated_storage": {
|
||||
"type": "number",
|
||||
"description": "Allocated storage in GB (default 20).",
|
||||
"required": false,
|
||||
"default": 20
|
||||
},
|
||||
"db_name": {
|
||||
"type": "string",
|
||||
"description": "The database name (some engines have restrictions).",
|
||||
"required": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "Master username.",
|
||||
"required": true
|
||||
},
|
||||
"multi_az": {
|
||||
"type": "boolean",
|
||||
"description": "Multi-AZ deployment (default false).",
|
||||
"required": false,
|
||||
"default": false
|
||||
},
|
||||
"storage_encrypted": {
|
||||
"type": "boolean",
|
||||
"description": "Enable storage encryption (default true).",
|
||||
"required": false,
|
||||
"default": true
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"description": "AWS region the RDS instance is created in.",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for storage encryption; if absent, uses AWS-managed key.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"db_endpoint": {
|
||||
"type": "string",
|
||||
"description": "The RDS instance endpoint (host:port)."
|
||||
},
|
||||
"db_arn": {
|
||||
"type": "arn",
|
||||
"description": "The RDS instance ARN."
|
||||
}
|
||||
},
|
||||
"nfrs": {
|
||||
"backup_retention_period": {
|
||||
"type": "number",
|
||||
"description": "Backup retention period in days (default 7).",
|
||||
"default": 7
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Enable deletion protection (default true for prod).",
|
||||
"default": true
|
||||
},
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable storage encryption.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,45 @@ pipeline as the regression baseline).
|
||||
- **Public access block** — add `aws_s3_bucket_public_access_block` to prevent data exfiltration (SOC2 CC6.1, GDPR Art.32).
|
||||
- **Lifecycle policy** — add `aws_s3_bucket_lifecycle_configuration` for retention enforcement (GDPR Art.5(2), HIPAA §164.530(j)).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: s3
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: my-simple-bucket
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with optional inputs:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: s3
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: my-production-bucket
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
> **Note:** the s3 primitive's compliance extensions (Object Lock, access
|
||||
> logging, public access block, lifecycle policy) are documented in the
|
||||
> Compliance extension points section above but not yet wired as inputs.
|
||||
> The complex example uses the same inputs as the simple example;
|
||||
> compliance extensions are roadmap.
|
||||
|
||||
## Versioning
|
||||
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: s3
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: my-production-bucket
|
||||
region: us-east-1
|
||||
@@ -0,0 +1,6 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.6
|
||||
module: s3
|
||||
environment: dev
|
||||
inputs:
|
||||
bucket_name: my-simple-bucket
|
||||
region: us-east-1
|
||||
@@ -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."}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
"type": "string",
|
||||
"description": "AWS region the bucket is created in.",
|
||||
"required": true
|
||||
},
|
||||
"kms_key_arn": {
|
||||
"type": "string",
|
||||
"description": "ARN of the CMK for SSE-KMS; if absent, uses managed key.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -24,6 +29,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": {
|
||||
@@ -31,6 +40,21 @@
|
||||
"type": "boolean",
|
||||
"description": "Enable S3 versioning (default true).",
|
||||
"default": true
|
||||
},
|
||||
"encryption_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable server-side encryption.",
|
||||
"default": true
|
||||
},
|
||||
"sse_algorithm": {
|
||||
"type": "string",
|
||||
"description": "SSE algorithm.",
|
||||
"default": "aws:kms"
|
||||
},
|
||||
"deletion_protection": {
|
||||
"type": "boolean",
|
||||
"description": "Prevent resource destruction via Terraform lifecycle prevent_destroy",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
# uptime — Uptime Kuma monitoring service
|
||||
|
||||
> **Module kind:** primitive | **Version:** 1.0.0
|
||||
|
||||
Uptime-kuma is a self-hosted monitoring tool deployed as an ECS Fargate
|
||||
container. It supports HTTP, DNS, and TCP health checks and can notify
|
||||
on-call via Teams, email, SMS, or GitHub issues. The module is deployed
|
||||
by default after any L2 module with a separate terraform state and can
|
||||
be disabled via the `feature_flag_enabled` input.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Type | Purpose |
|
||||
|----------|------|---------|
|
||||
| uptime | `aws:ecs:uptime-service` | ECS Fargate task + service + ALB + EFS volume |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `container_image` | string | no | `louislam/uptime-kuma:1` | Docker image for uptime-kuma |
|
||||
| `region` | string | yes | — | AWS region |
|
||||
| `uptime_url` | string | no | — | Custom domain for the uptime dashboard (optional; if absent, the ALB DNS is used) |
|
||||
| `monitored_endpoints` | array | no | `[]` | Array of endpoints to monitor. Each entry: {name, url, type (http\|dns\|tcp), interval_seconds, timeout_seconds} |
|
||||
| `static_checks` | array | no | `[]` | Pre-defined health checks (hardcoded monitors that don't depend on L2 outputs). Same shape as monitored_endpoints. |
|
||||
| `alert_channels` | object | no | `{}` | Alert notification channels. Keys: teams_webhook (string), email_addresses (array of strings), sms_numbers (array of strings), github_issue_repo (string, org/repo format) |
|
||||
| `feature_flag_enabled` | boolean | no | `true` | Feature flag: when false, no resources are emitted (the uptime deployment is skipped entirely) |
|
||||
| `cpu` | number | no | `256` | CPU units for the ECS task (256 = 0.25 vCPU) |
|
||||
| `memory` | number | no | `512` | Memory for the ECS task in MB |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `uptime_url` | string | The URL of the uptime-kuma dashboard (ALB DNS or custom domain) |
|
||||
| `service_arn` | arn | The ARN of the ECS service |
|
||||
| `task_definition_arn` | arn | The ARN of the ECS task definition |
|
||||
|
||||
## NFRs
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `deletion_protection` | boolean | true | Prevent resource destruction via Terraform lifecycle prevent_destroy |
|
||||
| `encryption_enabled` | boolean | true | Enable CloudWatch log group encryption with KMS |
|
||||
|
||||
## Usage
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uptime",
|
||||
"type": "aws:ecs:uptime-service",
|
||||
"module": "uptime@1.0.0",
|
||||
"inputs": {
|
||||
"container_image": "louislam/uptime-kuma:1",
|
||||
"region": "us-east-1",
|
||||
"feature_flag_enabled": true,
|
||||
"cpu": 256,
|
||||
"memory": 512,
|
||||
"monitored_endpoints": [
|
||||
{"name": "example", "url": "https://example.com", "type": "http", "interval_seconds": 60, "timeout_seconds": 30}
|
||||
],
|
||||
"alert_channels": {
|
||||
"teams_webhook": "https://hooks.example.com/webhook",
|
||||
"email_addresses": ["oncall@example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A concrete instance is at `instance.json` (used by the platform
|
||||
pipeline as the regression baseline).
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
- **KMS encryption for EFS** — encrypt the EFS volume that persists uptime-kuma state with a customer-managed KMS key (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||
- **HTTPS/TLS for the ALB** — attach an ACM certificate and HTTPS listener to the ALB so the dashboard is served over TLS (SOC2 CC6.1, GDPR Art.32).
|
||||
- **WAF in front of uptime dashboard** — place a WAF web ACL in front of the ALB to protect the dashboard from common exploits (SOC2 CC7.2).
|
||||
- **Secrets Manager for alert webhook URLs** — store Teams webhook URLs and other credentials in AWS Secrets Manager rather than plaintext inputs (SOC2 CC6.1, GDPR Art.32).
|
||||
- **CloudWatch alarms for uptime-kuma health** — add CloudWatch alarms on ECS task health and ALB 5xx rates to alert when the monitoring tool itself is degraded (SOC2 CC7.2, DORA Art.11).
|
||||
|
||||
## Examples
|
||||
|
||||
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||
pipeline validates them against `schemas/contract.schema.json`.
|
||||
|
||||
### Simple
|
||||
|
||||
A minimal deployment:
|
||||
|
||||
[`examples/simple.yaml`](examples/simple.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: uptime
|
||||
environment: dev
|
||||
inputs:
|
||||
region: us-east-1
|
||||
feature_flag_enabled: true
|
||||
```
|
||||
|
||||
### Complex
|
||||
|
||||
A production deployment with monitored endpoints, static checks, and
|
||||
multiple alert channels:
|
||||
|
||||
[`examples/complex.yaml`](examples/complex.yaml)
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: uptime
|
||||
environment: dev
|
||||
inputs:
|
||||
region: us-east-1
|
||||
feature_flag_enabled: true
|
||||
cpu: 512
|
||||
memory: 1024
|
||||
monitored_endpoints:
|
||||
- name: "api-health"
|
||||
url: "https://api.example.com/health"
|
||||
type: "http"
|
||||
interval_seconds: 30
|
||||
timeout_seconds: 10
|
||||
- name: "dns-check"
|
||||
url: "example.com"
|
||||
type: "dns"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
- name: "tcp-check"
|
||||
url: "db.example.com:5432"
|
||||
type: "tcp"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
static_checks:
|
||||
- name: "google"
|
||||
url: "https://google.com"
|
||||
type: "http"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
alert_channels:
|
||||
teams_webhook: "https://hooks.example.com/teams/webhook"
|
||||
email_addresses:
|
||||
- "oncall@example.com"
|
||||
- "sre@example.com"
|
||||
sms_numbers:
|
||||
- "+1234567890"
|
||||
github_issue_repo: "acdl/acdl"
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,38 @@
|
||||
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||
module: uptime
|
||||
environment: dev
|
||||
inputs:
|
||||
region: us-east-1
|
||||
feature_flag_enabled: true
|
||||
cpu: 512
|
||||
memory: 1024
|
||||
monitored_endpoints:
|
||||
- name: "api-health"
|
||||
url: "https://api.example.com/health"
|
||||
type: "http"
|
||||
interval_seconds: 30
|
||||
timeout_seconds: 10
|
||||
- name: "dns-check"
|
||||
url: "example.com"
|
||||
type: "dns"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
- name: "tcp-check"
|
||||
url: "db.example.com:5432"
|
||||
type: "tcp"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
static_checks:
|
||||
- name: "google"
|
||||
url: "https://google.com"
|
||||
type: "http"
|
||||
interval_seconds: 60
|
||||
timeout_seconds: 10
|
||||
alert_channels:
|
||||
teams_webhook: "https://hooks.example.com/teams/webhook"
|
||||
email_addresses:
|
||||
- "oncall@example.com"
|
||||
- "sre@example.com"
|
||||
sms_numbers:
|
||||
- "+1234567890"
|
||||
github_issue_repo: "acdl/acdl"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user