procurement-ingest/scripts/plat86-cfn-dispose.sh
Adam Moussa 2a2929b835
chore(cd): hard-cut CDK deploy; HCP is sole mutate path (PLAT-86)
Remove push-to-main cd-cdk workflow, document HCP apply + dispose runbook,
and park githubdeploy-procurement-ingest for a later IAM cleanup.
2026-08-07 11:31:34 -04:00

183 lines
6.7 KiB
Bash
Executable file

#!/usr/bin/env bash
# PLAT-86: dispose former CDK CloudFormation stacks after HCP Terraform import.
#
# Import-in-place only. Sets DeletionPolicy=Retain on every resource so stack
# delete orphans CFN ownership without destroying live TF-managed resources.
# Custom::S3BucketNotifications must be retained — its Delete handler would
# empty PutBucketNotificationConfiguration and wipe TF-owned inbound triggers.
#
# Usage:
# AWS_PROFILE=seahaven-prod ./scripts/plat86-cfn-dispose.sh --dry-run
# AWS_PROFILE=seahaven-prod ./scripts/plat86-cfn-dispose.sh --execute
#
# Never pairs with seahaven-org-baseline cfn-stack-decommission.sh --execute
# (that script deletes RETAIN orphans after stack delete).
set -euo pipefail
REGION="${AWS_REGION:-us-east-1}"
PROFILE="${AWS_PROFILE:-seahaven-prod}"
STACKS=(po-ingest WorkorderIngestStack procurement-api)
PO_BUCKET="po-ingest-emails-011934824531"
WO_BUCKET="workorder-ingest-emails-011934824531"
MODE=""
aws_cmd() {
aws --profile "${PROFILE}" --region "${REGION}" "$@"
}
usage() {
echo "Usage: $0 --dry-run | --execute" >&2
exit 2
}
[[ $# -eq 1 ]] || usage
case "$1" in
--dry-run) MODE=dry-run ;;
--execute) MODE=execute ;;
*) usage ;;
esac
work_dir="$(mktemp -d)"
trap 'rm -rf "${work_dir}"' EXIT
echo "==> mode=${MODE} profile=${PROFILE} region=${REGION}"
verify_notifications() {
local bucket="$1" expect_id="$2" expect_fn="$3"
local id fn
id="$(aws_cmd s3api get-bucket-notification-configuration --bucket "${bucket}" \
--query 'LambdaFunctionConfigurations[0].Id' --output text)"
fn="$(aws_cmd s3api get-bucket-notification-configuration --bucket "${bucket}" \
--query 'LambdaFunctionConfigurations[0].LambdaFunctionArn' --output text)"
if [[ "${id}" != "${expect_id}" ]] || [[ "${fn}" != *":function:${expect_fn}" ]]; then
echo "FAIL: ${bucket} notification mismatch id=${id} fn=${fn}" >&2
return 1
fi
echo "OK: ${bucket} -> ${id} (${expect_fn})"
}
verify_core() {
local fn
for fn in po-email-processor workorder-email-processor procurement-api \
po-ingest-site-extractor workorder-shoc-emitter; do
aws_cmd lambda get-function --function-name "${fn}" --query 'Configuration.FunctionName' --output text >/dev/null
echo "OK: lambda ${fn}"
done
for table in purchase-orders verified-sites pending-site-review WorkOrders WorkOrderComments; do
aws_cmd dynamodb describe-table --table-name "${table}" --query 'Table.TableName' --output text >/dev/null
echo "OK: table ${table}"
done
aws_cmd s3api head-bucket --bucket "${PO_BUCKET}" >/dev/null
aws_cmd s3api head-bucket --bucket "${WO_BUCKET}" >/dev/null
echo "OK: email buckets"
verify_notifications "${PO_BUCKET}" "po-email-processor-inbound" "po-email-processor"
verify_notifications "${WO_BUCKET}" "wo-email-processor-inbound" "workorder-email-processor"
}
echo "==> pre-checks"
verify_core
retain_template() {
local stack="$1"
local raw="${work_dir}/${stack}.raw.json"
local out="${work_dir}/${stack}.retain.json"
aws_cmd cloudformation get-template --stack-name "${stack}" --template-stage Original \
--query TemplateBody --output json >"${raw}"
python3 - "${raw}" "${out}" <<'PY'
import json, sys
raw_path, out_path = sys.argv[1], sys.argv[2]
body = json.load(open(raw_path))
# get-template may return already-parsed dict or a JSON string
if isinstance(body, str):
body = json.loads(body)
resources = body.get("Resources") or {}
changed = 0
for name, res in resources.items():
if not isinstance(res, dict):
continue
before = (res.get("DeletionPolicy"), res.get("UpdateReplacePolicy"))
res["DeletionPolicy"] = "Retain"
res["UpdateReplacePolicy"] = "Retain"
if before != ("Retain", "Retain"):
changed += 1
print(f"{len(resources)} resources; {changed} policy fields updated")
json.dump(body, open(out_path, "w"))
PY
}
wait_stack() {
local stack="$1" want="$2"
aws_cmd cloudformation wait "stack-${want}" --stack-name "${stack}"
local status
status="$(aws_cmd cloudformation describe-stacks --stack-name "${stack}" \
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo DELETE_COMPLETE)"
echo "stack ${stack} -> ${status}"
case "${status}" in
*COMPLETE) ;;
*) echo "FAIL: unexpected status ${status}" >&2; exit 1 ;;
esac
}
for stack in "${STACKS[@]}"; do
echo "==> retain-all template for ${stack}"
retain_template "${stack}"
if [[ "${MODE}" == "dry-run" ]]; then
echo "dry-run: would update-stack ${stack} then delete-stack"
continue
fi
echo "==> update-stack ${stack} (retain-all)"
aws_cmd cloudformation update-stack \
--stack-name "${stack}" \
--template-body "file://${work_dir}/${stack}.retain.json" \
--capabilities CAPABILITY_NAMED_IAM \
>/dev/null
wait_stack "${stack}" "update-complete"
echo "==> delete-stack ${stack}"
aws_cmd cloudformation delete-stack --stack-name "${stack}"
wait_stack "${stack}" "delete-complete"
echo "==> post-delete verify after ${stack}"
verify_core
done
if [[ "${MODE}" == "dry-run" ]]; then
echo "dry-run complete; no stacks modified"
exit 0
fi
echo "==> sweep BucketNotificationsHandler Lambdas (CDK helpers only)"
mapfile -t handlers < <(aws_cmd lambda list-functions \
--query "Functions[?contains(FunctionName, 'BucketNotificationsHandler')].FunctionName" \
--output text | tr '\t' '\n' | grep -E 'po-ingest-|WorkorderIngestStack-' || true)
for h in "${handlers[@]:-}"; do
[[ -n "${h}" ]] || continue
echo "deleting helper lambda ${h}"
aws_cmd lambda delete-function --function-name "${h}"
done
echo "==> sweep leftover BucketNotificationsHandler IAM roles"
mapfile -t roles < <(aws_cmd iam list-roles \
--query "Roles[?contains(RoleName, 'BucketNotificationsHandler')].RoleName" \
--output text | tr '\t' '\n' | grep -E 'po-ingest-|WorkorderIngestStack-' || true)
for role in "${roles[@]:-}"; do
[[ -n "${role}" ]] || continue
echo "deleting helper role ${role}"
# Detach inline + managed then delete
mapfile -t inlines < <(aws_cmd iam list-role-policies --role-name "${role}" --query 'PolicyNames[]' --output text | tr '\t' '\n')
for p in "${inlines[@]:-}"; do
[[ -n "${p}" ]] || continue
aws_cmd iam delete-role-policy --role-name "${role}" --policy-name "${p}"
done
mapfile -t attached < <(aws_cmd iam list-attached-role-policies --role-name "${role}" --query 'AttachedPolicies[].PolicyArn' --output text | tr '\t' '\n')
for a in "${attached[@]:-}"; do
[[ -n "${a}" ]] || continue
aws_cmd iam detach-role-policy --role-name "${role}" --policy-arn "${a}"
done
aws_cmd iam delete-role --role-name "${role}"
done
echo "==> final verify + smoke"
verify_core
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
AWS_PROFILE="${PROFILE}" AWS_REGION="${REGION}" bash "${ROOT}/scripts/post-deploy-smoke.sh"
echo "PLAT-86 CFN dispose complete"