Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 225de0f613 | |||
| 69d8496107 | |||
| 1aa525f234 |
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}...")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+85
-1
@@ -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
|
||||||
Reference in New Issue
Block a user