Compare commits

...

5 Commits

Author SHA1 Message Date
Jon Chery cc97a9308d docs(P09): complete iam-policy-least-privilege phase (v1.13.12)
---ci---
project: acdl
phase: 9
milestone: v1.14
status: complete
requirements:
  covered: [REQ-143]
  partial: []
---/ci---
2026-07-29 20:49:36 +00:00
Jon Chery c2ca0e4631 docs(P08): complete account-id-externalization phase (v1.13.11)
---ci---
project: acdl
phase: 8
milestone: v1.14
status: complete
requirements:
  covered: [REQ-142]
  partial: []
---/ci---
2026-07-29 20:46:28 +00:00
Jon Chery 225de0f613 docs(P07): complete swallowed-error-hardening phase (v1.13.10)
---ci---
project: acdl
phase: 7
milestone: v1.14
status: complete
requirements:
  covered: [REQ-141]
  partial: []
---/ci---
2026-07-29 20:43:08 +00:00
Jon Chery 69d8496107 docs(P06): complete alb-name-prefix-fix phase (v1.13.9)
---ci---
project: acdl
phase: 6
milestone: v1.14
status: complete
requirements:
  covered: [REQ-140]
  partial: []
---/ci---
2026-07-29 20:38:14 +00:00
Jon Chery 1aa525f234 docs(P05): complete adapter-behavior-tests phase (v1.13.8)
---ci---
project: acdl
phase: 5
milestone: v1.14
status: complete
requirements:
  covered: [REQ-139]
  partial: []
---/ci---
2026-07-29 20:35:53 +00:00
13 changed files with 178 additions and 27 deletions
+4 -2
View File
@@ -112,6 +112,8 @@ def adapt(stack_instance, out_dir):
stack_name = stack.get("name", "spike") stack_name = stack.get("name", "spike")
environment = stack.get("environment", "dev") environment = stack.get("environment", "dev")
account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
terraform_tf = ( terraform_tf = (
'terraform {\n' 'terraform {\n'
' required_version = ">= 1.9, < 1.10"\n' ' required_version = ">= 1.9, < 1.10"\n'
@@ -122,7 +124,7 @@ def adapt(stack_instance, out_dir):
' }\n' ' }\n'
' }\n' ' }\n'
' backend "s3" {\n' ' backend "s3" {\n'
' bucket = "acdl-tfstate-581513795199-us-east-1"\n' f' bucket = "{state_bucket}"\n'
f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n' f' key = "spike/{stack_name}/{environment}/terraform.tfstate"\n'
' region = "us-east-1"\n' ' region = "us-east-1"\n'
' }\n' ' }\n'
@@ -137,7 +139,7 @@ def adapt(stack_instance, out_dir):
'data "terraform_remote_state" "platform" {\n' 'data "terraform_remote_state" "platform" {\n'
' backend = "s3"\n' ' backend = "s3"\n'
' config = {\n' ' config = {\n'
' bucket = "acdl-tfstate-581513795199-us-east-1"\n' f' bucket = "{state_bucket}"\n'
f' key = "{remote_state_key}"\n' f' key = "{remote_state_key}"\n'
' region = "us-east-1"\n' ' region = "us-east-1"\n'
' }\n' ' }\n'
+11 -1
View File
@@ -17,6 +17,7 @@ requests. The invoke policy is scoped via ABAC (consumer repo identity).
import datetime import datetime
import json import json
import os import os
import urllib.error
import urllib.parse import urllib.parse
import boto3 import boto3
@@ -154,7 +155,16 @@ def _report_error(payload):
with urllib.request.urlopen(req, timeout=10) as resp: with urllib.request.urlopen(req, timeout=10) as resp:
search_result = json.loads(resp.read()) search_result = json.loads(resp.read())
existing = search_result.get("items", []) existing = search_result.get("items", [])
except Exception: except urllib.error.HTTPError as e:
if e.code == 404:
existing = []
else:
import sys
print(f"WARNING: GitHub issue search failed (HTTP {e.code}): {e}", file=sys.stderr)
existing = []
except urllib.error.URLError as e:
import sys
print(f"WARNING: GitHub issue search network error: {e}", file=sys.stderr)
existing = [] existing = []
body = f"""## Deploy Failure Report body = f"""## Deploy Failure Report
+3 -2
View File
@@ -371,8 +371,9 @@ class LocalLambdaStub:
return _FakeResponse( return _FakeResponse(
json.dumps([{"number": 1, "title": "stub"}]).encode()) json.dumps([{"number": 1, "title": "stub"}]).encode())
urllib.request.urlopen = _fake_urlopen urllib.request.urlopen = _fake_urlopen
except Exception: except (AttributeError, TypeError) as e:
pass import sys
print(f"WARNING: could not patch urlopen for local Lambda stub: {e}", file=sys.stderr)
try: try:
event = { event = {
+7 -3
View File
@@ -97,8 +97,10 @@ def publish_to_ssm(outputs, environment, contract_id):
Overwrite=True, Overwrite=True,
) )
results[name] = param_name results[name] = param_name
except Exception: except Exception as e:
# Don't fail the pipeline if one output fails to publish # Don't fail the pipeline if one output fails to publish, but log it
import sys
print(f"WARNING: SSM put_parameter failed for {name}: {e}", file=sys.stderr)
results[name] = None results[name] = None
return results return results
@@ -165,7 +167,9 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
req.add_header("Accept", "application/vnd.github+json") req.add_header("Accept", "application/vnd.github+json")
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
return True return True
except Exception: except Exception as e:
import sys
print(f"WARNING: GitHub PR comment failed: {e}", file=sys.stderr)
return False return False
+4 -2
View File
@@ -421,8 +421,10 @@ def _check_s3_state_bucket() -> Tuple[Status, str]:
s3 = boto3.client("s3", region_name=env.get("AWS_DEFAULT_REGION", "us-east-1"), 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_access_key_id=env.get("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY")) aws_secret_access_key=env.get("AWS_SECRET_ACCESS_KEY"))
s3.head_bucket(Bucket="acdl-tfstate-581513795199-us-east-1") account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
r = s3.list_objects_v2(Bucket="acdl-tfstate-581513795199-us-east-1", MaxKeys=5) state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
s3.head_bucket(Bucket=state_bucket)
r = s3.list_objects_v2(Bucket=state_bucket, MaxKeys=5)
keys = [o["Key"] for o in r.get("Contents", [])] keys = [o["Key"] for o in r.get("Contents", [])]
return "Verified", f"state bucket exists, keys={keys}" return "Verified", f"state bucket exists, keys={keys}"
except Exception as e: except Exception as e:
+1 -1
View File
@@ -6,7 +6,7 @@ resource "aws_lb" "this" {
} }
resource "aws_lb_target_group" "this" { resource "aws_lb_target_group" "this" {
name_prefix = "tg-ci-" name_prefix = "${var.name}-"
port = var.port port = var.port
protocol = var.protocol protocol = var.protocol
vpc_id = var.vpc_id vpc_id = var.vpc_id
+1 -1
View File
@@ -18,7 +18,7 @@
"wires": [ "wires": [
{"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"}, {"from": "contract.inputs.name", "to": "alb.inputs.name", "default": "app"},
{"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"}, {"from": "contract.inputs.name", "to": "ecr.inputs.name", "default": "app-repo"},
{"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "app-role"}, {"from": "contract.inputs.name", "to": "roles.inputs.role_name", "default": "acdl-app-role"},
{"from": "contract.inputs.region", "to": "cluster.inputs.region"}, {"from": "contract.inputs.region", "to": "cluster.inputs.region"},
{"from": "contract.inputs.region", "to": "ecr.inputs.region"}, {"from": "contract.inputs.region", "to": "ecr.inputs.region"},
{"from": "contract.inputs.region", "to": "roles.inputs.region"}, {"from": "contract.inputs.region", "to": "roles.inputs.region"},
+1 -1
View File
@@ -29,7 +29,7 @@ import boto3
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
ENV_FILE = REPO_ROOT / ".env.secrets" ENV_FILE = REPO_ROOT / ".env.secrets"
AWS_ACCOUNT_ID = "581513795199" AWS_ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
AWS_REGION = "us-east-1" AWS_REGION = "us-east-1"
ECR_REPO_NAME = "acdl-microservice" ECR_REPO_NAME = "acdl-microservice"
IMAGE_TAG = "latest" IMAGE_TAG = "latest"
+4 -2
View File
@@ -30,7 +30,7 @@ import boto3
ROOT = Path(__file__).resolve().parent.parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json" POLICY_PATH = ROOT / "terraform" / "bootstrap" / "spike_runner_policy.json"
ACCOUNT = "581513795199" ACCOUNT = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
USER = "acdl-spike-runner" USER = "acdl-spike-runner"
POLICY_NAME = "acdl-spike-runner-policy" POLICY_NAME = "acdl-spike-runner-policy"
POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}" POLICY_ARN = f"arn:aws:iam::{ACCOUNT}:policy/{POLICY_NAME}"
@@ -75,8 +75,10 @@ def apply_managed_policy(iam, policy_doc: str) -> str:
try: try:
iam.delete_policy_version(PolicyArn=POLICY_ARN, VersionId=default) iam.delete_policy_version(PolicyArn=POLICY_ARN, VersionId=default)
print(f"deleted old default version {default}") print(f"deleted old default version {default}")
except iam.exceptions.NoSuchEntityException:
pass # already deleted
except Exception as e: except Exception as e:
print(f"could not delete old version {default}: {e}") print(f"WARNING: could not delete old version {default}: {e}")
return POLICY_ARN return POLICY_ARN
except iam.exceptions.NoSuchEntityException: except iam.exceptions.NoSuchEntityException:
print(f"creating managed policy {POLICY_NAME}...") print(f"creating managed policy {POLICY_NAME}...")
+12 -8
View File
@@ -30,9 +30,9 @@ import boto3
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
STATE_BUCKET = "acdl-tfstate-581513795199-us-east-1" ACCOUNT_ID = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
STATE_BUCKET = f"acdl-tfstate-{ACCOUNT_ID}-us-east-1"
OUTBOX_TABLE = "acdl-outbox" OUTBOX_TABLE = "acdl-outbox"
ACCOUNT_ID = "581513795199"
def main(): def main():
@@ -48,12 +48,16 @@ def main():
try: try:
s3.head_bucket(Bucket=STATE_BUCKET) s3.head_bucket(Bucket=STATE_BUCKET)
print(f"s3: bucket {STATE_BUCKET} already exists") print(f"s3: bucket {STATE_BUCKET} already exists")
except Exception: except s3.exceptions.ClientError as e:
kwargs = {"Bucket": STATE_BUCKET} error_code = e.response.get("Error", {}).get("Code", "")
if REGION != "us-east-1": if error_code in ("404", "NoSuchBucket", "NotFound"):
kwargs["CreateBucketConfiguration"] = {"LocationConstraint": REGION} kwargs = {"Bucket": STATE_BUCKET}
s3.create_bucket(**kwargs) if REGION != "us-east-1":
print(f"s3: created bucket {STATE_BUCKET}") kwargs["CreateBucketConfiguration"] = {"LocationConstraint": REGION}
s3.create_bucket(**kwargs)
print(f"s3: created bucket {STATE_BUCKET}")
else:
raise
# Enable versioning (idempotent) # Enable versioning (idempotent)
s3.put_bucket_versioning( s3.put_bucket_versioning(
Bucket=STATE_BUCKET, Bucket=STATE_BUCKET,
+5 -2
View File
@@ -215,7 +215,10 @@
"kms:TagResource", "kms:TagResource",
"kms:UntagResource" "kms:UntagResource"
], ],
"Resource": "*" "Resource": [
"arn:aws:kms:*:*:key/*",
"arn:aws:kms:*:*:alias/acdl-*"
]
}, },
{ {
"Effect": "Allow", "Effect": "Allow",
@@ -233,7 +236,7 @@
"iam:TagRole", "iam:TagRole",
"iam:UntagRole" "iam:UntagRole"
], ],
"Resource": "*" "Resource": "arn:aws:iam::*:role/acdl-*"
} }
] ]
} }
+85 -1
View File
@@ -361,4 +361,88 @@ class TestAdapterDedupRejectsUnregisteredModule:
"data_sources": [], "data_sources": [],
} }
adapt(stack, str(tmp_path)) 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
+40 -1
View File
@@ -176,4 +176,43 @@ class TestIAMPolicyBaseline:
res = s.get("Resource", "") res = s.get("Resource", "")
if isinstance(res, list): if isinstance(res, list):
res = " ".join(res) res = " ".join(res)
assert res != "*", "iam:PassRole must not be granted to Resource: *" assert res != "*", "iam:PassRole must not be granted to Resource: *"
def test_iam_role_creation_scoped_to_acdl_prefix(self, policy):
"""G-104: iam:CreateRole must be scoped to role/acdl-* (not Resource: *)."""
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if "iam:CreateRole" in acts:
res = s.get("Resource", "")
if isinstance(res, list):
res = " ".join(res)
assert "acdl-*" in res, f"iam:CreateRole must be scoped to acdl-* (got: {res})"
def test_kms_scoped_to_acdl_alias(self, policy):
"""G-104: kms:CreateKey etc. must be scoped to alias/acdl-* (not Resource: *)."""
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if any(a.startswith("kms:") for a in acts):
res = s.get("Resource", "")
if isinstance(res, list):
res = " ".join(res)
assert "acdl-*" in res, f"kms actions must be scoped to acdl-* (got: {res})"
def test_cloudfront_waf_remain_global(self, policy):
"""G-104: CloudFront + WAFv2 (CloudFront scope) ARNs are global;
Resource: * is acceptable here (documented constraint, not a defect)."""
global_actions = {"cloudfront:", "wafv2:"}
for s in policy["Statement"]:
acts = s.get("Action", [])
if isinstance(acts, str):
acts = [acts]
if any(any(a.startswith(g) for g in global_actions) for a in acts):
res = s.get("Resource", "")
if isinstance(res, list):
res = res[0] if res else ""
# CloudFront/WAFv2 are allowed to be * (global ARNs)
assert res == "*" or "acdl" in res