feat(P32): deletion-protection-by-default + L2 feature flag (REQ-86, REQ-87)
---ci---
project: acdl
phase: 32
milestone: v1.8
status: execute
---/ci---
- All 11 L1 primitives now have deletion_protection NFR (boolean, default true).
- Adapter emits `lifecycle { prevent_destroy = true }` when NFR is true;
omits it when false. Default is true when NFR is absent.
- L2 composition resolver propagates inputs.deletion_protection to all
children NFRs. When false, all resources get deletion_protection=false.
- Stack schema updated with optional features object (deletion_protection,
uptime_enabled).
- Contract schema description updated to document deletion_protection
and uptime_enabled inputs.
Tests: +5 (307 -> 312). All pass.
This commit is contained in:
+91
-1
@@ -495,4 +495,94 @@ class TestEncryptionByDefault:
|
||||
assert "aws:kms:key" in types
|
||||
s3_res = next(r for r in stack["resources"] if r["type"] == "aws:s3:bucket")
|
||||
assert "kms_key_arn" in s3_res.get("inputs", {}), \
|
||||
"s3 must have kms_key_arn wired from the per-stack CMK"
|
||||
"s3 must have kms_key_arn wired from the per-stack CMK"
|
||||
|
||||
|
||||
class TestDeletionProtectionByDefault:
|
||||
"""REQ-86: deletion_protection NFR on all primitives (default true).
|
||||
REQ-87: L2 feature flag propagation."""
|
||||
|
||||
def test_all_l1_primitives_have_deletion_protection_nfr(self, registry, repo_root):
|
||||
"""REQ-86: every L1 primitive must have a deletion_protection NFR."""
|
||||
for name, entry in registry.items():
|
||||
iface_path = entry["1.0.0"]["interface"]
|
||||
if not iface_path.startswith("modules/l1/"):
|
||||
continue
|
||||
iface = json.load(open(os.path.join(str(repo_root), iface_path)))
|
||||
assert "deletion_protection" in iface.get("nfrs", {}), \
|
||||
f"L1 primitive '{name}' must have deletion_protection NFR"
|
||||
|
||||
def test_adapter_emits_prevent_destroy_when_nfr_true(self, tmp_path):
|
||||
"""REQ-86: adapter emits lifecycle { prevent_destroy = true } when NFR is true."""
|
||||
s3_stack = {
|
||||
"version": "1.0.0",
|
||||
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
|
||||
"resources": [{
|
||||
"id": "s3",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1"},
|
||||
"outputs": {},
|
||||
"nfrs": {"deletion_protection": True, "encryption_enabled": True, "versioning": True},
|
||||
}],
|
||||
}
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(s3_stack, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert "prevent_destroy = true" in main_tf
|
||||
|
||||
def test_adapter_omits_prevent_destroy_when_nfr_false(self, tmp_path):
|
||||
"""REQ-86: adapter does not emit prevent_destroy when NFR is false."""
|
||||
s3_stack = {
|
||||
"version": "1.0.0",
|
||||
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
|
||||
"resources": [{
|
||||
"id": "s3",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1"},
|
||||
"outputs": {},
|
||||
"nfrs": {"deletion_protection": False, "encryption_enabled": True, "versioning": True},
|
||||
}],
|
||||
}
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(s3_stack, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert "prevent_destroy = true" not in main_tf
|
||||
|
||||
def test_adapter_emits_prevent_destroy_by_default(self, tmp_path):
|
||||
"""REQ-86: when deletion_protection NFR is absent, default is true."""
|
||||
s3_stack = {
|
||||
"version": "1.0.0",
|
||||
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
|
||||
"resources": [{
|
||||
"id": "s3",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1"},
|
||||
"outputs": {},
|
||||
"nfrs": {},
|
||||
}],
|
||||
}
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(s3_stack, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert "prevent_destroy = true" in main_tf
|
||||
|
||||
def test_l2_feature_flag_propagates_deletion_protection_false(self, tmp_path):
|
||||
"""REQ-87: L2 feature flag deletion_protection=false propagates to all children."""
|
||||
import yaml
|
||||
contract = {
|
||||
"uses": "acdl/pipelines/deploy.yaml@v1.8",
|
||||
"module": "static-assets",
|
||||
"environment": "dev",
|
||||
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1", "deletion_protection": False},
|
||||
}
|
||||
contract_path = tmp_path / "test-dp.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
from core.contract_resolver import resolve
|
||||
stack = resolve(str(contract_path), str(ROOT))
|
||||
for res in stack["resources"]:
|
||||
assert res.get("nfrs", {}).get("deletion_protection") is False, \
|
||||
f"Resource {res['id']} should have deletion_protection=false"
|
||||
Reference in New Issue
Block a user