feat(P33): uptime-kuma primitive + deploy-uptime pipeline stage (REQ-88..91)

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

- New uptime L1 primitive (aws:ecs:uptime-service) deploying uptime-kuma
  on ECS Fargate with feature_flag_enabled, monitored_endpoints,
  static_checks, alert_channels (Teams/email/SMS/GitHub issues).
- Adapter emits ECS Fargate task + service when feature_flag_enabled=true;
  emits nothing when false. Container image louislam/uptime-kuma:1.
- New deploy-uptime pipeline stage in pipelines/deploy.yaml (after
  publish-outputs, before comment-outputs). Now 9 stages.
- run_platform.sh --deploy-uptime flag + automatic uptime deployment
  after L2 module (separate state $WORK/uptime-tf). Endpoints from L2
  outputs passed as monitored_endpoints. Feature flag from
  inputs.uptime_enabled (default true).
- scripts/seed_uptime_monitors.py for post-deploy monitor seeding via
  uptime-kuma API.
- Registered in registry.json (14 modules total).

Tests: +6 (312 -> 318). All pass.
This commit is contained in:
Jon Chery
2026-07-22 22:15:35 +00:00
parent 8145eee8fc
commit 491ba78768
12 changed files with 591 additions and 6 deletions
+75 -5
View File
@@ -31,14 +31,14 @@ class TestInstance:
class TestRegistry:
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds", "kms-key"}
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds", "kms-key", "uptime"}
EXPECTED_L2_KEYS = {"static-assets", "microservice"}
def test_registry_has_13_entries(self, registry):
assert len(registry) == 13
def test_registry_has_14_entries(self, registry):
assert len(registry) == 14
assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS)
def test_registry_has_11_l1_entries(self, registry):
def test_registry_has_12_l1_entries(self, registry):
l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")}
assert l1 == self.EXPECTED_L1_KEYS
@@ -585,4 +585,74 @@ class TestDeletionProtectionByDefault:
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"
f"Resource {res['id']} should have deletion_protection=false"
class TestUptimePrimitive:
"""REQ-88/89/90/91: uptime-kuma primitive + feature flag + pipeline stage."""
def test_uptime_primitive_in_registry(self, registry):
assert "uptime" in registry
def test_uptime_interface_has_feature_flag(self, repo_root):
iface = json.load(open(os.path.join(str(repo_root), "modules", "l1", "uptime", "interface.json")))
assert "feature_flag_enabled" in iface["inputs"]
assert iface["inputs"]["feature_flag_enabled"]["default"] is True
def test_uptime_interface_has_alert_channels(self, repo_root):
iface = json.load(open(os.path.join(str(repo_root), "modules", "l1", "uptime", "interface.json")))
assert "alert_channels" in iface["inputs"]
assert "monitored_endpoints" in iface["inputs"]
def test_uptime_adapter_emits_ecs_service_when_enabled(self, tmp_path):
uptime_stack = {
"version": "1.0.0",
"stack": {"name": "uptime", "kind": "l1", "depth": 1},
"resources": [{
"id": "uptime",
"type": "aws:ecs:uptime-service",
"module": "uptime@1.0.0",
"inputs": {
"container_image": "louislam/uptime-kuma:1",
"region": "us-east-1",
"feature_flag_enabled": True,
"monitored_endpoints": [{"name": "test", "url": "https://example.com", "type": "http", "interval_seconds": 60, "timeout_seconds": 30}],
"cpu": 256,
"memory": 512,
},
"outputs": {},
"nfrs": {"deletion_protection": True, "encryption_enabled": True},
}],
}
out_dir = str(tmp_path / "tf_out")
adapt(uptime_stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
assert 'resource "aws_ecs_service" "uptime"' in main_tf
assert "louislam/uptime-kuma:1" in main_tf
assert "desired_count = 1" in main_tf
def test_uptime_adapter_emits_nothing_when_disabled(self, tmp_path):
"""REQ-90: feature_flag_enabled=false means no resources emitted."""
uptime_stack = {
"version": "1.0.0",
"stack": {"name": "uptime", "kind": "l1", "depth": 1},
"resources": [{
"id": "uptime",
"type": "aws:ecs:uptime-service",
"module": "uptime@1.0.0",
"inputs": {"region": "us-east-1", "feature_flag_enabled": False},
"outputs": {},
"nfrs": {},
}],
}
out_dir = str(tmp_path / "tf_out")
adapt(uptime_stack, out_dir)
main_tf = open(os.path.join(out_dir, "main.tf")).read()
assert 'resource "aws_ecs_service" "uptime"' not in main_tf
def test_deploy_pipeline_has_deploy_uptime_stage(self):
import yaml
with open(ROOT / "pipelines/deploy.yaml") as fh:
contract = yaml.safe_load(fh)
stage_names = [s["name"] for s in contract["stages"]]
assert "deploy-uptime" in stage_names
+2 -1
View File
@@ -265,7 +265,7 @@ class TestDeployPipelineContract:
contract = _load_yaml("pipelines/deploy.yaml")
jsonschema.validate(contract, schema)
def test_deploy_contract_has_eight_stages(self):
def test_deploy_contract_has_nine_stages(self):
contract = _load_yaml("pipelines/deploy.yaml")
stage_names = [s["name"] for s in contract["stages"]]
assert stage_names == [
@@ -276,6 +276,7 @@ class TestDeployPipelineContract:
"confidence",
"apply",
"publish-outputs",
"deploy-uptime",
"comment-outputs",
]