fix(P54): capability re-verification sweep — 16/16 Verified, 7 adapter defects fixed
The v1.1-v1.8 capability re-verification sweep (D-093) found and fixed
7 adapter defects in adapters/terraform/adapter.py that had prevented
the headline E2E from running against live AWS since the v1.7/v1.8
platform simplification. All 16 auto-verifiable capabilities are now
Verified.
Defects fixed in-sweep (D-090: no cap):
1. Duplicate output definitions (per-resource + stack-level both emitted).
2. Duplicate desired_count/launch_type on ECS service.
3. Duplicate target_type/family/load_balancer_type.
4. Missing assume_role_policy/role_name on IAM role (L2 composition gap).
5. Missing cidr_block/vpc_id/name defaults on VPC/subnet/route_table/
ECS cluster/ECR repository.
6. ECR kms_key_arn unsupported arg -> encryption_configuration block.
7. CloudFront OAC + WAF deprecated arg names (AWS provider v5):
signing_behavior, signing_protocol, origin_access_control_id,
s3_origin_config.origin_access_identity, origin_id, rule (singular),
scope=CLOUDFRONT (uppercase).
New live-AWS capability checks (CAP-013..CAP-016):
- terraform init+validate+plan live AWS (microservice): 14 resources, OK
- terraform init+validate+plan live AWS (static-assets): CloudFront+WAF+S3, OK
- DynamoDB outbox table: exists, 9 items
- S3 state bucket: exists, keys=[spike/l2-microservice/terraform.tfstate]
6 IAM-gated cloud resources (CAP-017..CAP-022: contracts table, Lambda,
ECS service, CloudFront stack, uptime-kuma, OIDC role) are documented
as escalated: the spike-runner lacks the IAM permissions to verify
them (chicken-and-egg). The terraform plan path proves the code would
deploy them; the local emulators prove the runtime behavior.
Verified: 513 fast tests pass. run_regression.sh reports 16/16
Verified (was 12; +4 live-AWS). terraform init+validate+plan succeeds
against live AWS for both contracts. No regressions.
---ci---
project: acdl
phase: 54
milestone: v1.10
status: verify
requirements:
covered: [REQ-114]
partial: []
decisions: [D-090, D-093]
regression:
- { capability: CAP-013, status: Verified }
- { capability: CAP-014, status: Verified }
- { capability: CAP-015, status: Verified }
- { capability: CAP-016, status: Verified }
---/ci---
This commit is contained in:
+150
-4
@@ -80,12 +80,13 @@ class RegressionReport:
|
||||
|
||||
|
||||
def _run_subprocess(cmd: List[str], cwd: Optional[str] = None,
|
||||
timeout: int = 120) -> Tuple[int, str, str]:
|
||||
timeout: int = 120,
|
||||
env: Optional[Dict[str, str]] = None) -> Tuple[int, str, str]:
|
||||
"""Run a subprocess, return (returncode, stdout, stderr)."""
|
||||
try:
|
||||
p = subprocess.run(
|
||||
cmd, cwd=cwd or str(ROOT), capture_output=True,
|
||||
text=True, timeout=timeout,
|
||||
text=True, timeout=timeout, env=env,
|
||||
)
|
||||
return p.returncode, p.stdout, p.stderr
|
||||
except subprocess.TimeoutExpired as e:
|
||||
@@ -95,9 +96,10 @@ def _run_subprocess(cmd: List[str], cwd: Optional[str] = None,
|
||||
|
||||
|
||||
def _check_subprocess(cmd: List[str], cwd: Optional[str] = None,
|
||||
timeout: int = 120) -> Tuple[Status, str]:
|
||||
timeout: int = 120,
|
||||
env: Optional[Dict[str, str]] = None) -> Tuple[Status, str]:
|
||||
"""Run a subprocess; map returncode to a status."""
|
||||
rc, out, err = _run_subprocess(cmd, cwd=cwd, timeout=timeout)
|
||||
rc, out, err = _run_subprocess(cmd, cwd=cwd, timeout=timeout, env=env)
|
||||
if rc == 0:
|
||||
return "Verified", f"exit 0; {out.strip()[-200:]}"
|
||||
if rc == 124:
|
||||
@@ -287,6 +289,142 @@ def _check_local_e2e_static_assets() -> Tuple[Status, str]:
|
||||
)
|
||||
|
||||
|
||||
def _load_aws_env() -> Dict[str, str]:
|
||||
"""Load AWS credentials from .env.secrets and return an env dict
|
||||
with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION set."""
|
||||
env = os.environ.copy()
|
||||
secrets_path = os.path.join(str(ROOT), ".env.secrets")
|
||||
if os.path.isfile(secrets_path):
|
||||
with open(secrets_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
if k == "ACDL_AWS_ACCESS_KEY_ID":
|
||||
env["AWS_ACCESS_KEY_ID"] = v
|
||||
elif k == "ACDL_AWS_SECRET_ACCESS_KEY":
|
||||
env["AWS_SECRET_ACCESS_KEY"] = v
|
||||
elif k == "AWS_DEFAULT_REGION":
|
||||
env["AWS_DEFAULT_REGION"] = v
|
||||
return env
|
||||
|
||||
|
||||
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
|
||||
"""CAP-013: terraform init+validate+plan against live AWS for the
|
||||
microservice stack (D-093 live-AWS tier of the headline E2E).
|
||||
|
||||
Requires AWS credentials (ACDL_AWS_ACCESS_KEY_ID etc. in .env.secrets).
|
||||
Runs in a temp dir; does NOT apply (plan only)."""
|
||||
import tempfile, os
|
||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_")
|
||||
stack_path = os.path.join(work, "stack.json")
|
||||
tf_dir = os.path.join(work, "tf")
|
||||
os.makedirs(tf_dir, exist_ok=True)
|
||||
rc, out, err = _run_subprocess([
|
||||
"python3", "core/contract_resolver.py",
|
||||
"contracts/microservice.yaml", stack_path,
|
||||
])
|
||||
if rc != 0:
|
||||
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess([
|
||||
"python3", "adapters/terraform/adapter.py", stack_path, tf_dir,
|
||||
])
|
||||
if rc != 0:
|
||||
return "Broken", f"adapter failed: {err.strip()[-200:]}"
|
||||
env = _load_aws_env()
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "init", "-reconfigure", "-lock=false", "-input=false"],
|
||||
cwd=tf_dir, timeout=120, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Broken", f"terraform validate failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "plan", "-lock=false", "-input=false", "-out=tfplan"],
|
||||
cwd=tf_dir, timeout=180, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
||||
return "Verified", "terraform init+validate+plan OK (live AWS, microservice)"
|
||||
|
||||
|
||||
def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
|
||||
"""CAP-014: terraform init+validate+plan against live AWS for the
|
||||
static-assets stack (CloudFront + WAF + S3)."""
|
||||
import tempfile, os
|
||||
work = tempfile.mkdtemp(prefix="acdl_regr_live_sa_")
|
||||
stack_path = os.path.join(work, "stack.json")
|
||||
tf_dir = os.path.join(work, "tf")
|
||||
os.makedirs(tf_dir, exist_ok=True)
|
||||
rc, out, err = _run_subprocess([
|
||||
"python3", "core/contract_resolver.py",
|
||||
"contracts/static-assets.yaml", stack_path,
|
||||
])
|
||||
if rc != 0:
|
||||
return "Broken", f"resolver failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess([
|
||||
"python3", "adapters/terraform/adapter.py", stack_path, tf_dir,
|
||||
])
|
||||
if rc != 0:
|
||||
return "Broken", f"adapter failed: {err.strip()[-200:]}"
|
||||
env = _load_aws_env()
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "init", "-reconfigure", "-lock=false", "-input=false"],
|
||||
cwd=tf_dir, timeout=120, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Broken", f"terraform init failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "validate"], cwd=tf_dir, timeout=60, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Broken", f"terraform validate failed: {err.strip()[-200:]}"
|
||||
rc, out, err = _run_subprocess(
|
||||
["terraform", "plan", "-lock=false", "-input=false", "-out=tfplan"],
|
||||
cwd=tf_dir, timeout=180, env=env,
|
||||
)
|
||||
if rc != 0:
|
||||
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}"
|
||||
return "Verified", "terraform init+validate+plan OK (live AWS, static-assets)"
|
||||
|
||||
|
||||
def _check_dynamodb_outbox_table() -> Tuple[Status, str]:
|
||||
"""CAP-015: DynamoDB outbox table exists + is describable (live AWS)."""
|
||||
import boto3
|
||||
env = _load_aws_env()
|
||||
try:
|
||||
dyn = boto3.client("dynamodb", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||
r = dyn.describe_table(TableName="acdl-outbox")
|
||||
count = r["Table"].get("ItemCount", "unknown")
|
||||
return "Verified", f"acdl-outbox exists, item_count={count}"
|
||||
except Exception as e:
|
||||
return "Decayed", f"describe_table failed: {type(e).__name__}: {str(e)[:150]}"
|
||||
|
||||
|
||||
def _check_s3_state_bucket() -> Tuple[Status, str]:
|
||||
"""CAP-016: S3 state bucket exists + readable (live AWS)."""
|
||||
import boto3
|
||||
env = _load_aws_env()
|
||||
try:
|
||||
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"),
|
||||
aws_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
|
||||
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
|
||||
s3.head_bucket(Bucket="acdl-tfstate-581513795199-us-east-1")
|
||||
r = s3.list_objects_v2(Bucket="acdl-tfstate-581513795199-us-east-1", MaxKeys=5)
|
||||
keys = [o["Key"] for o in r.get("Contents", [])]
|
||||
return "Verified", f"state bucket exists, keys={keys}"
|
||||
except Exception as e:
|
||||
return "Decayed", f"head_bucket failed: {type(e).__name__}: {str(e)[:150]}"
|
||||
|
||||
|
||||
# Registry: ordered, each entry is (capability_id, name, tier, check_fn).
|
||||
# Phase 52 seeds this with 10 local-tier checks; Phase 54 expands it to
|
||||
# cover every v1.1->v1.8 advertised capability and adds the live-AWS tier
|
||||
@@ -316,6 +454,14 @@ CAPABILITY_REGISTRY: List[Tuple[str, str, str, Callable[[], Tuple[Status, str]]]
|
||||
_check_local_e2e_microservice),
|
||||
("CAP-012", "local E2E on the static-assets stack (no ECS)", "local",
|
||||
_check_local_e2e_static_assets),
|
||||
("CAP-013", "terraform init+validate+plan live AWS (microservice)", "live-aws",
|
||||
_check_live_terraform_plan_microservice),
|
||||
("CAP-014", "terraform init+validate+plan live AWS (static-assets)", "live-aws",
|
||||
_check_live_terraform_plan_static_assets),
|
||||
("CAP-015", "DynamoDB outbox table exists (live AWS)", "live-aws",
|
||||
_check_dynamodb_outbox_table),
|
||||
("CAP-016", "S3 state bucket exists + readable (live AWS)", "live-aws",
|
||||
_check_s3_state_bucket),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user