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:
@@ -39,6 +39,7 @@ cd "$ROOT"
|
||||
CHECK_ONLY=0
|
||||
PLAN_ONLY=0
|
||||
QUIET=0
|
||||
DEPLOY_UPTIME=0
|
||||
CONTRACT=""
|
||||
|
||||
for arg in "$@"; do
|
||||
@@ -46,6 +47,7 @@ for arg in "$@"; do
|
||||
--check-only) CHECK_ONLY=1 ;;
|
||||
--plan-only) PLAN_ONLY=1 ;;
|
||||
--quiet) QUIET=1 ;;
|
||||
--deploy-uptime) DEPLOY_UPTIME=1 ;;
|
||||
--*) echo "FAIL: unknown flag: $arg" >&2; exit 1 ;;
|
||||
*) CONTRACT="$arg" ;;
|
||||
esac
|
||||
@@ -290,6 +292,73 @@ PY
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Step 9b: deploy uptime monitoring (separate state) ==="
|
||||
# The uptime stack is deployed by default after the L2 module. It uses a
|
||||
# separate terraform state ($WORK/uptime-tf). Endpoints from the L2 outputs
|
||||
# are passed as monitored_endpoints. The feature flag (inputs.uptime_enabled,
|
||||
# default true) controls whether this step runs.
|
||||
if [ "$DEPLOY_UPTIME" = "1" ] || ( [ "$CHECK_ONLY" = "0" ] && [ "$PLAN_ONLY" = "0" ] ); then
|
||||
UPTIME_ENABLED=$(python3 -c "import yaml; c=yaml.safe_load(open('$CONTRACT')); print(c.get('inputs',{}).get('uptime_enabled', True))" 2>/dev/null || echo "True")
|
||||
if [ "$UPTIME_ENABLED" = "True" ] || [ "$UPTIME_ENABLED" = "true" ]; then
|
||||
echo "uptime: feature flag enabled — constructing uptime contract"
|
||||
UPTIME_DIR="$WORK/uptime-tf"
|
||||
mkdir -p "$UPTIME_DIR"
|
||||
# Build the uptime stack from the L2 outputs
|
||||
python3 "$ROOT/core/contract_resolver.py" "$CONTRACT" "$WORK/stack.json" 2>/dev/null || true
|
||||
python3 -c "
|
||||
import json, sys, yaml
|
||||
sys.path.insert(0, '$ROOT')
|
||||
from core.contract_resolver import resolve
|
||||
stack = resolve('$CONTRACT', '$ROOT')
|
||||
# Extract HTTP/DNS/TCP endpoints from the stack outputs
|
||||
endpoints = []
|
||||
outputs = stack.get('outputs', {})
|
||||
for name, spec in outputs.items():
|
||||
src_rid = spec.get('from', '')
|
||||
src_output = spec.get('output', name)
|
||||
if 'domain' in name.lower() or 'url' in name.lower() or 'endpoint' in name.lower():
|
||||
endpoints.append({
|
||||
'name': name,
|
||||
'url': f'ref:{src_rid}.{src_output}',
|
||||
'type': 'http',
|
||||
'interval_seconds': 60,
|
||||
'timeout_seconds': 30
|
||||
})
|
||||
# Build the uptime contract
|
||||
uptime_contract = {
|
||||
'uses': 'acdl/pipelines/deploy.yaml@v1.8',
|
||||
'module': 'uptime',
|
||||
'environment': 'dev',
|
||||
'inputs': {
|
||||
'region': 'us-east-1',
|
||||
'feature_flag_enabled': True,
|
||||
'monitored_endpoints': endpoints,
|
||||
}
|
||||
}
|
||||
with open('$WORK/uptime-contract.yaml', 'w') as f:
|
||||
yaml.dump(uptime_contract, f)
|
||||
print(f'uptime: {len(endpoints)} endpoint(s) to monitor')
|
||||
" 2>/dev/null || echo "uptime: no endpoints found (skipping monitor config)"
|
||||
|
||||
# Resolve + adapt the uptime contract to a separate TF dir
|
||||
python3 "$ROOT/core/contract_resolver.py" "$WORK/uptime-contract.yaml" "$WORK/uptime-stack.json" 2>/dev/null || true
|
||||
python3 "$ROOT/adapters/terraform/adapter.py" "$WORK/uptime-stack.json" "$UPTIME_DIR" 2>/dev/null || true
|
||||
|
||||
if [ "$DEPLOY_UPTIME" = "1" ] && [ -f "$UPTIME_DIR/main.tf" ]; then
|
||||
echo "uptime: emitted Terraform to $UPTIME_DIR"
|
||||
if [ "$QUIET" = "0" ]; then
|
||||
echo "--- uptime main.tf ---"
|
||||
cat "$UPTIME_DIR/main.tf"
|
||||
echo "--- end uptime main.tf ---"
|
||||
fi
|
||||
fi
|
||||
echo "uptime: monitoring stack ready (separate state: $UPTIME_DIR)"
|
||||
else
|
||||
echo "uptime: feature flag disabled (inputs.uptime_enabled=false) — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== PLATFORM E2E OK ==="
|
||||
echo "contract -> resolver -> stack -> terraform plan -> Checkov -> confidence ($BAND) -> outbox -> outputs"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seed uptime-kuma monitors via the API after the ECS service is up.
|
||||
|
||||
Reads monitored_endpoints from a JSON file and creates monitors in the
|
||||
uptime-kuma instance via its REST API. Used by the deploy-uptime pipeline
|
||||
stage after the ECS service is running.
|
||||
|
||||
Usage:
|
||||
seed_uptime_monitors.py <endpoints.json> <uptime_url> [--token <token>]
|
||||
|
||||
The endpoints.json file is an array of:
|
||||
{"name": "...", "url": "...", "type": "http|dns|tcp",
|
||||
"interval_seconds": 60, "timeout_seconds": 30}
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
def create_monitor(uptime_url, endpoint, token=None):
|
||||
"""Create a single monitor in uptime-kuma via the API."""
|
||||
monitor_type_map = {
|
||||
"http": "http",
|
||||
"https": "http",
|
||||
"tcp": "port",
|
||||
"dns": "dns",
|
||||
"ping": "ping",
|
||||
}
|
||||
monitor_type = monitor_type_map.get(endpoint.get("type", "http"), "http")
|
||||
payload = {
|
||||
"name": endpoint["name"],
|
||||
"type": monitor_type,
|
||||
"url": endpoint["url"],
|
||||
"interval": endpoint.get("interval_seconds", 60),
|
||||
"timeout": endpoint.get("timeout_seconds", 30),
|
||||
}
|
||||
url = f"{uptime_url}/api/monitor"
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
if token:
|
||||
req.add_header("Authorization", f"Bearer {token}")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return {"status": "created", "monitor": endpoint["name"], "result": result}
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"status": "error", "monitor": endpoint["name"], "error": str(e)}
|
||||
except Exception as e:
|
||||
return {"status": "error", "monitor": endpoint["name"], "error": str(e)}
|
||||
|
||||
|
||||
def seed_monitors(endpoints, uptime_url, token=None):
|
||||
"""Seed all monitors from the endpoints list."""
|
||||
results = []
|
||||
for endpoint in endpoints:
|
||||
result = create_monitor(uptime_url, endpoint, token)
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Seed uptime-kuma monitors")
|
||||
parser.add_argument("endpoints_file", help="JSON file with monitored endpoints")
|
||||
parser.add_argument("uptime_url", help="URL of the uptime-kuma instance")
|
||||
parser.add_argument("--token", default=None, help="Auth token (if required)")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.endpoints_file) as f:
|
||||
endpoints = json.load(f)
|
||||
|
||||
results = seed_monitors(endpoints, args.uptime_url, args.token)
|
||||
for r in results:
|
||||
print(f" [{r['status']}] {r['monitor']}")
|
||||
created = sum(1 for r in results if r["status"] == "created")
|
||||
print(f"Seeded {created}/{len(results)} monitors")
|
||||
Reference in New Issue
Block a user