mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 08:23:14 +00:00
Inline template-body exceeds the 51KB CloudFormation CLI limit; upload retain-all templates to the artifacts bucket and update via URL.
190 lines
7.2 KiB
Bash
Executable file
190 lines
7.2 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"
|
|
# Staging for retain-all templates (inline --template-body is capped at 51KB).
|
|
TEMPLATE_BUCKET="${PLAT86_TEMPLATE_BUCKET:-procurement-ingest-artifacts-011934824531}"
|
|
TEMPLATE_PREFIX="plat86-cfn-dispose"
|
|
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 via s3://${TEMPLATE_BUCKET})"
|
|
key="${TEMPLATE_PREFIX}/${stack}-retain-$(date -u +%Y%m%dT%H%M%SZ).json"
|
|
aws_cmd s3 cp "${work_dir}/${stack}.retain.json" "s3://${TEMPLATE_BUCKET}/${key}" >/dev/null
|
|
# us-east-1 regional URL form required by CloudFormation.
|
|
template_url="https://${TEMPLATE_BUCKET}.s3.${REGION}.amazonaws.com/${key}"
|
|
aws_cmd cloudformation update-stack \
|
|
--stack-name "${stack}" \
|
|
--template-url "${template_url}" \
|
|
--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"
|