feat(P34): decommission alias + CMDB validation (REQ-92, REQ-93, REQ-94)

---ci---
project: acdl
phase: 34
milestone: v1.8
status: execute
---/ci---

- DynamoDB acdl-change-requests table added to terraform/platform/main.tf
  (PK changeRequestId, SK submittedAt, SSE via CMK, PITR).
- validate_change_request Lambda action added to contract_ingestor.py:
  queries CMDB, asserts status=approved + consumerRepo match.
- decommission_transform() added to contract_resolver.py: zeroes all
  counts (desired_count, min/max_capacity) + sets deletion_protection=false.
- Decommission mode added to deploy pipeline + both deploy workflows
  (mode: decommission + changeRequestId input). Byte-identical.
- run_platform.sh --decommission flag: validates CR, resolves with
  deletion_protection=false (step 1), then decommission_transform
  (step 2). HITL SRE gates documented.
- docs/consumer-guide.md: new "Decommissioning a stack" section with
  CR request, trigger, 2-step HITL SRE gates, CMK deletion window, uptime.

Tests: +14 (318 -> 332). All pass.
This commit is contained in:
Jon Chery
2026-07-22 22:18:28 +00:00
parent 491ba78768
commit 134f85d2df
10 changed files with 458 additions and 7 deletions
+84 -1
View File
@@ -417,4 +417,87 @@ class TestForgeAgnosticApiUrls:
def test_comments_url_uses_api_base(self, monkeypatch):
monkeypatch.setattr(ingestor, "GITHUB_API_BASE", "https://git.cloudinit.dev/api/v1")
url = ingestor._issue_comments_url("acdl", "acdl", 42)
assert url == "https://git.cloudinit.dev/api/v1/repos/acdl/acdl/issues/42/comments"
assert url == "https://git.cloudinit.dev/api/v1/repos/acdl/acdl/issues/42/comments"
class TestValidateChangeRequest:
"""REQ-93: validate_change_request Lambda action (CMDB validation)."""
@pytest.fixture
def moto_change_requests_table(self, monkeypatch):
from moto import mock_aws
import boto3
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("CHANGE_REQUESTS_TABLE", "acdl-change-requests")
with mock_aws():
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.create_table(
TableName="acdl-change-requests",
KeySchema=[
{"AttributeName": "changeRequestId", "KeyType": "HASH"},
{"AttributeName": "submittedAt", "KeyType": "RANGE"},
],
AttributeDefinitions=[
{"AttributeName": "changeRequestId", "AttributeType": "S"},
{"AttributeName": "submittedAt", "AttributeType": "S"},
],
BillingMode="PAY_PER_REQUEST",
)
# Insert an approved CR
table.put_item(Item={
"changeRequestId": "CR-001",
"submittedAt": "2026-07-22T10:00:00Z",
"consumerRepo": "acdl/consumer-a",
"contractId": "contract-001",
"status": "approved",
"requestedBy": "developer",
"approvedBy": "sre",
})
# Insert a pending CR
table.put_item(Item={
"changeRequestId": "CR-002",
"submittedAt": "2026-07-22T11:00:00Z",
"consumerRepo": "acdl/consumer-b",
"contractId": "contract-002",
"status": "requested",
"requestedBy": "developer",
})
# Reset the module's dynamodb client so it picks up the moto mock
ingestor._dynamodb = None
yield
def test_validates_approved_cr(self, moto_change_requests_table):
payload = {"changeRequestId": "CR-001", "consumerRepo": "acdl/consumer-a"}
result = ingestor._validate_change_request(payload)
assert result["status"] == "approved"
assert result["changeRequestId"] == "CR-001"
def test_rejects_non_approved_cr(self, moto_change_requests_table):
payload = {"changeRequestId": "CR-002", "consumerRepo": "acdl/consumer-b"}
with pytest.raises(ValueError, match="status is 'requested'"):
ingestor._validate_change_request(payload)
def test_rejects_nonexistent_cr(self, moto_change_requests_table):
payload = {"changeRequestId": "CR-NONEXIST", "consumerRepo": "acdl/consumer-a"}
with pytest.raises(ValueError, match="not found in CMDB"):
ingestor._validate_change_request(payload)
def test_rejects_repo_mismatch(self, moto_change_requests_table):
payload = {"changeRequestId": "CR-001", "consumerRepo": "acdl/wrong-repo"}
with pytest.raises(ValueError, match="consumerRepo mismatch"):
ingestor._validate_change_request(payload)
def test_lambda_handler_routes_validate_change_request(self, moto_change_requests_table):
event = {"body": json.dumps({
"action": "validate_change_request",
"changeRequestId": "CR-001",
"consumerRepo": "acdl/consumer-a",
})}
resp = ingestor.lambda_handler(event, None)
assert resp["statusCode"] == 200
body = json.loads(resp["body"])
assert body["action"] == "validate_change_request"