0ca383dae6
Create run_codegen.sh (pre-TF: env check, validate, resolve, adapt). Create run_postapply.sh (post-TF: Checkov, confidence, HITL, outbox, SSM, uptime). Add variable 'enabled' (bool, default true) + count=var.enabled?1:0 to all 12 L1 modules (alb, cloudfront, ecr, ecs-cluster, ecs-service, iam-role, kms-key, rds, s3, uptime, vpc, waf). Fix all cross-resource references with [0] indexing. Update interface.json for all modules to declare 'enabled' input. Fix stale artifact path /tmp/acdl_platform_run_v18 → /tmp/nova_platform_run (REQ-238). run_platform.sh remains as backward-compat shim for local-dev usage. ---ci--- project: acdl phase: 4 milestone: v1.20 status: execute requirements: [REQ-233, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238] ---/ci---
203 lines
8.1 KiB
Bash
Executable File
203 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# scripts/run_postapply.sh — post-Terraform steps for the Nova platform pipeline.
|
||
#
|
||
# Performs steps 5–9 of run_platform.sh (after terraform apply/destroy):
|
||
# 5. Checkov policy scan on the emitted main.tf
|
||
# 6. Checkov adapter → PolicyCheckResult (compliance details)
|
||
# 7. Confidence signal compute
|
||
# 7b. HITL attestation gate (qa/prod/dr only)
|
||
# 8. Write evidence event to DynamoDB outbox
|
||
# 9. Publish outputs to SSM + GitHub PR comment
|
||
# 9b. Uptime monitoring sub-deploy (sourced from run_uptime.sh)
|
||
#
|
||
# Expects the work dir ($NOVA_WORK_DIR) to already contain:
|
||
# - tf/*.tf (from run_codegen.sh)
|
||
# - stack.json (from run_codegen.sh)
|
||
# And terraform to have already run (init/validate/plan/apply) in $WORK/tf/.
|
||
#
|
||
# Usage:
|
||
# run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]
|
||
#
|
||
set -euo pipefail
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
CALLER_CWD="$(pwd)"
|
||
cd "$ROOT"
|
||
|
||
QUIET=0
|
||
DEPLOY_UPTIME=0
|
||
ENVIRONMENT_OVERRIDE=""
|
||
CONTRACT=""
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--quiet) QUIET=1; shift ;;
|
||
--deploy-uptime) DEPLOY_UPTIME=1; shift ;;
|
||
--environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;;
|
||
--environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;;
|
||
-h|--help)
|
||
echo "Usage: run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]"
|
||
exit 0 ;;
|
||
*) CONTRACT="$1"; shift ;;
|
||
esac
|
||
done
|
||
|
||
[ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; }
|
||
if ! [[ "$CONTRACT" = /* ]]; then
|
||
CONTRACT="$CALLER_CWD/$CONTRACT"
|
||
fi
|
||
[ -f "$CONTRACT" ] || { echo "FAIL: contract not found: $CONTRACT" >&2; exit 1; }
|
||
|
||
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
|
||
export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE"
|
||
fi
|
||
|
||
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}"
|
||
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
|
||
TF_DIR="$WORK/tf"
|
||
|
||
[ -d "$TF_DIR" ] || { echo "FAIL: work dir $TF_DIR not found (run run_codegen.sh first)" >&2; exit 1; }
|
||
|
||
stream() {
|
||
local log="$1"; shift
|
||
if [ "$QUIET" = "1" ]; then
|
||
"$@" > "$log" 2>&1
|
||
else
|
||
"$@" 2>&1 | tee "$log"
|
||
fi
|
||
}
|
||
|
||
run_hitl_gate() {
|
||
local _cid="$1" _env="$2" _ctx="$3"
|
||
if [ "$_env" = "dev" ]; then
|
||
echo "Environment is $_env — autonomous (no HITL gate)."
|
||
return 0
|
||
fi
|
||
echo "Environment is $_env — HITL attestation gate required$_ctx."
|
||
local _approver="${GITHUB_ACTOR:-${FORGE_ACTOR:-}}"
|
||
if [ -z "$_approver" ]; then
|
||
echo "WARNING: no approver identity (GITHUB_ACTOR/FORGE_ACTOR unset)" >&2
|
||
echo " the gate would block in a real CI run. Passing for local." >&2
|
||
fi
|
||
python3 -c "
|
||
import os, sys
|
||
sys.path.insert(0, '.')
|
||
from core.hitl_gates import attest
|
||
from core import env as _envhelper
|
||
ok, reason = attest('$_cid', '$_env', '$_approver')
|
||
if ok:
|
||
print(f'HITL: {reason}')
|
||
else:
|
||
print(f'HITL BLOCK: {reason}', file=sys.stderr)
|
||
sys.exit(1)
|
||
" || { echo "FAIL: HITL gate blocked" >&2; return 1; }
|
||
}
|
||
|
||
echo "=== Step 5: run Checkov on $TF_DIR/main.tf ==="
|
||
if [ "$QUIET" = "0" ]; then
|
||
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov.json"
|
||
else
|
||
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov.json" 2> "$WORK/checkov.err"
|
||
fi
|
||
[ -s "$WORK/checkov.json" ] || { echo "FAIL: checkov produced no output" >&2; exit 1; }
|
||
echo ""
|
||
echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
|
||
|
||
echo ""
|
||
echo "=== Step 6: Checkov adapter → PolicyCheckResult (compliance details) ==="
|
||
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || { echo "FAIL: checkov adapter failed" >&2; exit 1; }
|
||
python3 -c "
|
||
import json
|
||
pcrs = json.load(open('$WORK/pcr.json'))
|
||
print(f'PolicyCheckResult: {len(pcrs)} record(s)')
|
||
print()
|
||
for pcr in pcrs:
|
||
sev = pcr.get('severity', 'info')
|
||
res = pcr.get('result', 'unknown')
|
||
rule = pcr.get('ruleId', 'unknown')
|
||
msg = pcr.get('message', '')
|
||
marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper()
|
||
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
|
||
"
|
||
|
||
echo ""
|
||
echo "=== Step 7: confidence signal compute ==="
|
||
python3 <<PY > "$WORK/signal.json" || { echo "FAIL: confidence signal failed" >&2; exit 1; }
|
||
import json
|
||
import core.confidence_signal as c
|
||
pcr = json.load(open("$WORK/pcr.json"))
|
||
inputs = {
|
||
"policy": pcr,
|
||
"validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True},
|
||
"freshness": {"age_days": 0, "max_age_days": 7},
|
||
"source": {"submitter": "consumer", "commit_sha": "consumer-sha", "signed": False},
|
||
"history": {"prior_rollbacks": 0, "prior_policy_fails": 0},
|
||
"nfrs": {"conformance": None},
|
||
}
|
||
sig = c.compute("$CONTRACT_ID", "dev", inputs)
|
||
print(json.dumps({"score": sig.score, "band": sig.band, "perInput": sig.perInput, "reasonCodes": sig.reasonCodes}, indent=2))
|
||
PY
|
||
BAND=$(python3 -c "import json; print(json.load(open('$WORK/signal.json'))['band'])")
|
||
SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json'))['score'],3))")
|
||
echo "confidence: score=$SCORE band=$BAND"
|
||
[ "$BAND" = "pass" ] || { echo "FAIL: confidence band is $BAND, expected pass for dev" >&2; exit 1; }
|
||
|
||
echo ""
|
||
echo "=== Step 7b: HITL attestation gate (qa/prod/dr only) ==="
|
||
RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev")
|
||
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
|
||
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
|
||
fi
|
||
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
|
||
|
||
echo ""
|
||
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
|
||
STACK_NAME=$(python3 -c "import json; print(json.load(open('$WORK/stack.json'))['stack']['name'])")
|
||
python3 <<PY > "$WORK/event.json" || { echo "FAIL: event build failed" >&2; exit 1; }
|
||
import json, datetime
|
||
sig = json.load(open("$WORK/signal.json"))
|
||
event = {
|
||
"contractId": "$CONTRACT_ID",
|
||
"eventType": "CONFIDENCE_COMPUTED",
|
||
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||
"environment": "dev",
|
||
"stack": "$STACK_NAME",
|
||
"score": sig["score"],
|
||
"band": sig["band"],
|
||
"prev_event_hash": "GENESIS",
|
||
}
|
||
print(json.dumps(event, indent=2))
|
||
PY
|
||
python3 core/outbox_writer.py "$WORK/event.json" > "$WORK/outbox_item.json" || { echo "FAIL: outbox write failed" >&2; exit 1; }
|
||
echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.json')); print('contractId=', d['contractId'], 'hash=', d['hash'][:16]+'...')")"
|
||
|
||
echo ""
|
||
echo "=== Step 9: publish outputs to SSM + GitHub PR comment ==="
|
||
cd "$TF_DIR"
|
||
TF_OUTPUTS=$(terraform output -json 2>/dev/null || echo "{}")
|
||
cd "$ROOT"
|
||
python3 <<PY > "$WORK/outputs_step.json" 2>/dev/null || true
|
||
import json, sys
|
||
sys.path.insert(0, "$ROOT")
|
||
from core.output_publisher import publish_to_ssm, format_comment, post_github_comment
|
||
tf_raw = json.loads('''$TF_OUTPUTS''')
|
||
outputs = {k: v.get("value") if isinstance(v, dict) else v for k, v in tf_raw.items()}
|
||
ssm_results = publish_to_ssm(outputs, "dev", "$CONTRACT_ID")
|
||
comment = format_comment(outputs, "dev", "$CONTRACT_ID", ssm_results)
|
||
posted = post_github_comment(comment)
|
||
print(json.dumps({"ssm": ssm_results, "posted": posted, "comment": comment}))
|
||
PY
|
||
if [ -f "$WORK/outputs_step.json" ]; then
|
||
echo "outputs published to SSM: $(python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(len([v for v in d.get('ssm',{}).values() if v]), 'parameters')" 2>/dev/null || echo "done")"
|
||
if [ "$QUIET" = "0" ]; then
|
||
python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(d.get('comment',''))" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
# Uptime monitoring: sourced from run_uptime.sh
|
||
source "$ROOT/scripts/run_uptime.sh"
|
||
|
||
echo ""
|
||
echo "=== POST-APPLY OK ==="
|
||
echo "Checkov → confidence ($BAND) → outbox → outputs → uptime"
|