Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa525f234 | |||
| 81f111d462 | |||
| 79e7a4a304 | |||
| 8ae307affc |
@@ -93,22 +93,25 @@ down to zero-cost steady state (P64, D-096).
|
||||
|
||||
- **CAP-017 (Verified):** DynamoDB `acdl-contracts` table — Verified
|
||||
live-aws via L1 rds module lifecycle pipeline (apply/modify/destroy
|
||||
exit 0). Evidence: regression registry CAP-017 (lifecycle-pipeline tier).
|
||||
exit 0). Evidence: regression registry CAP-017 (offline proxy: terraform
|
||||
files present + fmt -check passes + contracts resolve; live
|
||||
apply/modify/destroy verified by the modules-lifecycle workflow run).
|
||||
- **CAP-018 (Verified):** Lambda contract-ingestor — Verified via local
|
||||
Lambda stub (CAP-011, Phase 53) + lifecycle pipeline. Evidence:
|
||||
regression registry CAP-018.
|
||||
regression registry CAP-018 (offline proxy).
|
||||
- **CAP-019 (Verified):** ECS cluster + service — Verified live-aws via
|
||||
L2 microservice lifecycle pipeline (apply/modify/destroy exit 0).
|
||||
Evidence: regression registry CAP-019.
|
||||
Evidence: regression registry CAP-019 (offline proxy).
|
||||
- **CAP-020 (Verified):** CloudFront + WAF production static-assets
|
||||
stack — Verified live-aws via L2 static-assets lifecycle pipeline
|
||||
(apply/modify/destroy exit 0). Evidence: regression registry CAP-020.
|
||||
(apply/modify/destroy exit 0). Evidence: regression registry CAP-020
|
||||
(offline proxy).
|
||||
- **CAP-021 (Verified):** uptime-kuma monitoring primitive — Verified
|
||||
live-aws via L1 uptime module lifecycle pipeline. Evidence: regression
|
||||
registry CAP-021.
|
||||
registry CAP-021 (offline proxy).
|
||||
- **CAP-022 (Verified):** OIDC role for act_runner — Verified live-aws
|
||||
via L1 iam-role module lifecycle pipeline. Evidence: regression
|
||||
registry CAP-022.
|
||||
registry CAP-022 (offline proxy).
|
||||
|
||||
All CAP-017..022 are now in the regression registry
|
||||
(`core/regression_verify.py`) with "lifecycle-pipeline" tier evidence
|
||||
|
||||
+24
-10
@@ -431,13 +431,19 @@ def _check_s3_state_bucket() -> Tuple[Status, str]:
|
||||
|
||||
def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
|
||||
"""Helper: verify an L1 module's terraform dir exists with the required
|
||||
files + its example contracts resolve. This is the offline proxy for
|
||||
'lifecycle pipeline green' — the pipeline cell going green requires
|
||||
terraform init+validate+apply+modify+destroy to succeed against live
|
||||
AWS, which requires the terraform files to exist and contracts to
|
||||
resolve first. We avoid terraform init here (too slow for the
|
||||
regression gate); terraform validate is run by the lifecycle pipeline
|
||||
itself."""
|
||||
files + its example contracts resolve + terraform fmt syntax check
|
||||
passes. This is the offline proxy for 'lifecycle pipeline green' — the
|
||||
pipeline cell going green requires terraform init+validate+apply+modify+
|
||||
destroy to succeed against live AWS, which requires the terraform files
|
||||
to exist, contracts to resolve, and HCL syntax to be valid first.
|
||||
|
||||
We run `terraform fmt -check` (fast, no init required) as a syntax probe.
|
||||
We avoid `terraform validate` here (requires `terraform init`, which
|
||||
downloads providers — too slow for the regression gate). Full
|
||||
`terraform validate` is run by the lifecycle pipeline itself. This is
|
||||
an offline proxy, not live pipeline evidence; the live apply/modify/
|
||||
destroy is verified by the modules-lifecycle workflow run, not by this
|
||||
gate."""
|
||||
tf_dir = ROOT / "modules" / "l1" / module / "terraform"
|
||||
if not tf_dir.is_dir():
|
||||
return "Broken", f"modules/l1/{module}/terraform/ does not exist"
|
||||
@@ -450,6 +456,11 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
|
||||
tf_text = "".join((tf_dir / f).read_text() for f in ["variables.tf", "main.tf", "outputs.tf"] if (tf_dir / f).is_file())
|
||||
if "local." in tf_text and not (tf_dir / "locals.tf").is_file():
|
||||
return "Broken", "missing terraform files: ['locals.tf'] (referenced by module)"
|
||||
# terraform fmt -check: fast HCL syntax probe (no init required).
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
|
||||
if rc != 0:
|
||||
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}"
|
||||
for ex in ["simple", "complex"]:
|
||||
contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml"
|
||||
if not contract.is_file():
|
||||
@@ -459,12 +470,15 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]:
|
||||
], timeout=30)
|
||||
if rc != 0:
|
||||
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
|
||||
return "Verified", f"terraform files present + simple/complex contracts resolve"
|
||||
return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve"
|
||||
|
||||
|
||||
def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
||||
"""Helper: verify an L2 module's composition resolves + its example
|
||||
contracts resolve. Offline proxy for 'L2 lifecycle pipeline green'."""
|
||||
contracts resolve. Offline proxy for 'L2 lifecycle pipeline green'.
|
||||
This is an offline proxy, not live pipeline evidence; the live
|
||||
apply/modify/destroy is verified by the modules-lifecycle workflow
|
||||
run, not by this gate."""
|
||||
for ex in ["simple", "complex"]:
|
||||
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml"
|
||||
if not contract.is_file():
|
||||
@@ -474,7 +488,7 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]:
|
||||
], timeout=30)
|
||||
if rc != 0:
|
||||
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}"
|
||||
return "Verified", f"L2 composition resolves (simple + complex contracts)"
|
||||
return "Verified", f"L2 composition resolves (simple + complex contracts; offline proxy)"
|
||||
|
||||
|
||||
def _check_cap_017_dynamodb() -> Tuple[Status, str]:
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
{"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"},
|
||||
{"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"},
|
||||
{"from": "contract.inputs.region", "to": "kms.inputs.region"},
|
||||
{"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"}
|
||||
{"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"},
|
||||
{"from": "contract.inputs.default_ttl", "to": "cloudfront.inputs.default_ttl"},
|
||||
{"from": "contract.inputs.max_ttl", "to": "cloudfront.inputs.max_ttl"},
|
||||
{"from": "contract.inputs.price_class", "to": "cloudfront.inputs.price_class"},
|
||||
{"from": "contract.inputs.viewer_protocol_policy", "to": "cloudfront.inputs.viewer_protocol_policy"}
|
||||
],
|
||||
"outputs": [
|
||||
{"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"},
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
# Complex static-assets deployment (S3 + CloudFront + WAF)
|
||||
# Modify variant: same bucket_name as simple (in-place modify, adds CDN + WAF)
|
||||
# Modify variant: same bucket_name as simple (in-place modify, tunes CDN
|
||||
# TTLs + price class + viewer protocol policy). The simple example uses
|
||||
# the cloudfront interface defaults (default_ttl=3600, max_ttl=86400,
|
||||
# PriceClass_100, redirect-to-https); this complex example sets explicit
|
||||
# non-default values so the lifecycle "modify" step exercises a real
|
||||
# terraform diff on the cloudfront distribution, not an idempotent
|
||||
# re-apply.
|
||||
environment: dev
|
||||
id: assets
|
||||
infrastructure:
|
||||
static-assets:
|
||||
inputs:
|
||||
bucket_name: my-static-site
|
||||
default_ttl: 3600
|
||||
max_ttl: 86400
|
||||
price_class: PriceClass_100
|
||||
default_ttl: 7200
|
||||
max_ttl: 172800
|
||||
price_class: PriceClass_200
|
||||
region: us-east-1
|
||||
viewer_protocol_policy: redirect-to-https
|
||||
waf_enabled: true
|
||||
viewer_protocol_policy: https-only
|
||||
version: 1.0.0
|
||||
name: static assets
|
||||
name: static assets
|
||||
@@ -1,11 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/run_l2_lifecycle_destroy.sh — run a single L2 module lifecycle destroy.
|
||||
#
|
||||
# Usage: run_l2_lifecycle_destroy.sh <module> [ci-vpc-outputs.json]
|
||||
# Usage: run_l2_lifecycle_destroy.sh <module>
|
||||
#
|
||||
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
|
||||
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state.
|
||||
#
|
||||
# NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does
|
||||
# NOT take a ci-vpc-outputs.json argument. L2 compositions reference the
|
||||
# platform VPC via terraform_remote_state (a data source), not by injecting
|
||||
# VPC outputs into the contract. The workflow passes 2 positional args for
|
||||
# parity with the L1 matrix, but $2 is accepted-but-ignored here (documented,
|
||||
# not a bug).
|
||||
#
|
||||
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" = no-op
|
||||
# (plan mode never applies resources, so there is nothing to destroy).
|
||||
# Set to "full" for the real `--destroy` against live AWS.
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/run_l2_lifecycle_test.sh — run a single L2 module lifecycle apply/modify.
|
||||
#
|
||||
# Usage: run_l2_lifecycle_test.sh <module> <example> [ci-vpc-outputs.json]
|
||||
# Usage: run_l2_lifecycle_test.sh <module> <example>
|
||||
#
|
||||
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
|
||||
# pipeline. Sets ACDL_REMOTE_STATE_KEY to point to the CI VPC state so the
|
||||
# microservice composition's terraform_remote_state data source reads from
|
||||
# the short-lived CI VPC (not the long-lived platform VPC).
|
||||
#
|
||||
# NOTE: unlike the L1 scripts (run_lifecycle_test.sh), the L2 path does NOT
|
||||
# take a ci-vpc-outputs.json argument. L2 compositions reference the platform
|
||||
# VPC via terraform_remote_state (a data source), not by injecting VPC
|
||||
# outputs into the contract. The ACDL_REMOTE_STATE_KEY env var points the
|
||||
# data source at the correct CI VPC state key. The workflow passes 3
|
||||
# positional args for parity with the L1 matrix, but $3 is accepted-but-
|
||||
# ignored here (documented, not a bug).
|
||||
#
|
||||
# Lifecycle mode (REQ-134): ACDL_LIFECYCLE_MODE default "plan" runs
|
||||
# `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for
|
||||
# the real `--apply` against live AWS.
|
||||
|
||||
+85
-1
@@ -361,4 +361,88 @@ class TestAdapterDedupRejectsUnregisteredModule:
|
||||
"data_sources": [],
|
||||
}
|
||||
adapt(stack, str(tmp_path))
|
||||
assert (tmp_path / "main.tf").exists()
|
||||
assert (tmp_path / "main.tf").exists()
|
||||
|
||||
|
||||
class TestAdapterDedupMergesSameModule:
|
||||
"""P2-2 (v1.14, REQ-139): two resources with the same module collapse
|
||||
to one module block named by the child id, with merged inputs. This
|
||||
locks in the dedup-merge behavior at the unit level."""
|
||||
|
||||
def test_two_resources_same_module_collapse_to_one_block(self, tmp_path):
|
||||
"""Two resources sharing the same terraform dir (e.g. cloudfront
|
||||
distribution + OAC) must produce ONE module block, not two."""
|
||||
stack = {
|
||||
"resources": [
|
||||
{"id": "cloudfront-distribution", "type": "aws:cloudfront:distribution", "module": "cloudfront@1.0.0", "inputs": {"price_class": "PriceClass_100"}},
|
||||
{"id": "cloudfront-originaccesscontrol", "type": "aws:cloudfront:originaccesscontrol", "module": "cloudfront@1.0.0", "inputs": {"viewer_protocol_policy": "redirect-to-https"}},
|
||||
],
|
||||
"outputs": {},
|
||||
"data_sources": [],
|
||||
}
|
||||
adapt(stack, str(tmp_path))
|
||||
main_tf = (tmp_path / "main.tf").read_text()
|
||||
# Exactly one module block for cloudfront (deduped to child id "cloudfront")
|
||||
assert main_tf.count('module "cloudfront" {') == 1
|
||||
# No separate module blocks for the expanded sub-ids
|
||||
assert 'module "cloudfront-distribution"' not in main_tf
|
||||
assert 'module "cloudfront-originaccesscontrol"' not in main_tf
|
||||
|
||||
def test_dedup_merges_inputs_from_both_resources(self, tmp_path):
|
||||
"""When two resources share a module, their inputs are merged into
|
||||
the single module block (first resource's inputs + second's, with
|
||||
first-wins for overlapping keys)."""
|
||||
stack = {
|
||||
"resources": [
|
||||
{"id": "cloudfront-distribution", "type": "aws:cloudfront:distribution", "module": "cloudfront@1.0.0", "inputs": {"price_class": "PriceClass_100", "region": "us-east-1"}},
|
||||
{"id": "cloudfront-originaccesscontrol", "type": "aws:cloudfront:originaccesscontrol", "module": "cloudfront@1.0.0", "inputs": {"viewer_protocol_policy": "redirect-to-https"}},
|
||||
],
|
||||
"outputs": {},
|
||||
"data_sources": [],
|
||||
}
|
||||
adapt(stack, str(tmp_path))
|
||||
main_tf = (tmp_path / "main.tf").read_text()
|
||||
# Both inputs present in the merged module block
|
||||
assert "PriceClass_100" in main_tf
|
||||
assert "redirect-to-https" in main_tf
|
||||
|
||||
|
||||
class TestAdapterRemoteStateKeyOverride:
|
||||
"""P2-2 (v1.14, REQ-139): ACDL_REMOTE_STATE_KEY env var overrides the
|
||||
default 'platform/terraform.tfstate' key in the emitted
|
||||
data terraform_remote_state block. This is the load-bearing correctness
|
||||
mechanism for the microservice L2 lifecycle (remote state points at the
|
||||
CI VPC, not the platform VPC)."""
|
||||
|
||||
def test_default_remote_state_key(self, tmp_path, monkeypatch):
|
||||
"""When ACDL_REMOTE_STATE_KEY is unset, the default key is used."""
|
||||
monkeypatch.delenv("ACDL_REMOTE_STATE_KEY", raising=False)
|
||||
stack = {
|
||||
"resources": [
|
||||
{"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test", "region": "us-east-1"}}
|
||||
],
|
||||
"outputs": {},
|
||||
"data_sources": ["platform"],
|
||||
}
|
||||
adapt(stack, str(tmp_path))
|
||||
terraform_tf = (tmp_path / "terraform.tf").read_text()
|
||||
main_tf = (tmp_path / "main.tf").read_text()
|
||||
# The remote state data block uses the default key
|
||||
assert "platform/terraform.tfstate" in main_tf
|
||||
|
||||
def test_env_override_remote_state_key(self, tmp_path, monkeypatch):
|
||||
"""When ACDL_REMOTE_STATE_KEY is set, the emitted data block uses
|
||||
the overridden key (e.g. 'spike/ci-vpc/terraform.tfstate')."""
|
||||
monkeypatch.setenv("ACDL_REMOTE_STATE_KEY", "spike/ci-vpc/terraform.tfstate")
|
||||
stack = {
|
||||
"resources": [
|
||||
{"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test", "region": "us-east-1"}}
|
||||
],
|
||||
"outputs": {},
|
||||
"data_sources": ["platform"],
|
||||
}
|
||||
adapt(stack, str(tmp_path))
|
||||
main_tf = (tmp_path / "main.tf").read_text()
|
||||
# The remote state data block uses the overridden key
|
||||
assert "spike/ci-vpc/terraform.tfstate" in main_tf
|
||||
assert "platform/terraform.tfstate" not in main_tf
|
||||
Reference in New Issue
Block a user