From 9b50029cb2e2e43036174b53456ddf99129a2404 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 5 Oct 2026 17:00:50 +0200 Subject: [PATCH] test(ci): tighten destroy-script guards and fixtures (#489) --- scripts/force-delete-owned-ecr-repo.sh | 13 ++++++++++- scripts/test-ecr-delete-selection.sh | 22 +++++++++++++++++++ scripts/test-rds-deletion-protection-scope.sh | 14 +++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/scripts/force-delete-owned-ecr-repo.sh b/scripts/force-delete-owned-ecr-repo.sh index a733a5db..624c4f84 100755 --- a/scripts/force-delete-owned-ecr-repo.sh +++ b/scripts/force-delete-owned-ecr-repo.sh @@ -80,6 +80,17 @@ if [[ "$OUTPUTS_LENGTH" -eq 0 ]]; then fi OWNED_REPO="$(jq -er '.ecr_repository_name.value' <<<"$OUTPUTS_JSON")" +# An empty string is neither null nor false, so `jq -er` above accepts it. +# Whitespace is refused too: no repository name contains any. +case "$OWNED_REPO" in + '' | *[![:graph:]]*) + echo "error: state '${STATE_DIR}' publishes 'ecr_repository_name', but it resolved" >&2 + echo " to '${OWNED_REPO}', which is not a repository name. Inspect" >&2 + echo " 'terraform -chdir=${STATE_DIR} output -json' before destroying anything." >&2 + exit 1 + ;; +esac + echo "This state owns ECR repository '$OWNED_REPO'" # Asks for the owned repository by name rather than listing the account. A @@ -96,7 +107,7 @@ DESCRIBE_EXIT=0 LISTING="$(aws ecr describe-repositories --repository-names "$OWNED_REPO" \ --query 'repositories[].repositoryName' --output text 2>"$DESCRIBE_ERR")" || DESCRIBE_EXIT=$? if [[ "$DESCRIBE_EXIT" -ne 0 ]]; then - if grep -q 'RepositoryNotFoundException' "$DESCRIBE_ERR"; then + if grep -q '(RepositoryNotFoundException)' "$DESCRIBE_ERR"; then echo "ECR repository '$OWNED_REPO' does not exist; it is already deleted and there is nothing to clean up." exit 0 fi diff --git a/scripts/test-ecr-delete-selection.sh b/scripts/test-ecr-delete-selection.sh index 41bb98a1..08e861af 100755 --- a/scripts/test-ecr-delete-selection.sh +++ b/scripts/test-ecr-delete-selection.sh @@ -702,6 +702,10 @@ case "$2" in echo "An error occurred (RepositoryNotFoundException) when calling the DescribeRepositories operation: The repository with name '${requested}' does not exist in the registry with id '111111111111'" >&2 exit 254 ;; + mention) + echo "An error occurred (AccessDeniedException) when calling the DescribeRepositories operation: not authorized; RepositoryNotFoundException is not the cause" >&2 + exit 254 + ;; error) echo "An error occurred (ThrottlingException) when calling the DescribeRepositories operation: Rate exceeded" >&2 exit 254 @@ -773,6 +777,14 @@ assert_behaviour "behaviour: the already-deleted case says so" \ "$([[ "$STUB_OUT" == *"already deleted"* ]] && echo 0 || echo 1)" \ "stdout: ${STUB_OUT}" +# The not-found match is anchored to the error name AWS prints, so a message +# that only mentions the word is not read as "already deleted". +export DESCRIBE_MODE=mention +run_script "$STUB_STATE" +assert_behaviour "behaviour: a non-NotFound error that mentions RepositoryNotFoundException still fails the step" \ + "$([[ "$STUB_EXIT" -ne 0 && "$STUB_OUT" != *"already deleted"* ]] && echo 0 || echo 1)" \ + "exit ${STUB_EXIT}, stdout: ${STUB_OUT}" + # Any other lookup error must fail the step and surface the AWS message, not # read as "already deleted". export DESCRIBE_MODE=error @@ -784,6 +796,16 @@ assert_behaviour "behaviour: the lookup error's AWS message reaches stderr" \ "$([[ "$STUB_ERR" == *"ThrottlingException"* ]] && echo 0 || echo 1)" \ "stderr: ${STUB_ERR}" +# An empty or whitespace name is refused before any AWS call. +for bad in '""' '"a b"'; do + export TF_OUTPUT_JSON="{\"ecr_repository_name\":{\"value\":${bad}}}" + run_script "$STUB_STATE" + assert_behaviour "behaviour: repository name ${bad} exits 1 with its own error and no aws call" \ + "$([[ "$STUB_EXIT" -eq 1 && ! -s "$STUB_CALLS" && "$STUB_ERR" == *"not a repository name"* ]] && echo 0 || echo 1)" \ + "exit ${STUB_EXIT}, stderr: ${STUB_ERR}, calls: $(cat "$STUB_CALLS")" +done +export TF_OUTPUT_JSON='{"ecr_repository_name":{"value":"cudly-dev-1a2b3c4d"}}' + export DESCRIBE_MODE=found DELETE_FAILS=1 run_script "$STUB_STATE" assert_behaviour "behaviour: a failed delete fails the step" \ diff --git a/scripts/test-rds-deletion-protection-scope.sh b/scripts/test-rds-deletion-protection-scope.sh index b7f76635..67b00038 100755 --- a/scripts/test-rds-deletion-protection-scope.sh +++ b/scripts/test-rds-deletion-protection-scope.sh @@ -533,6 +533,15 @@ assert_behaviour "behaviour: a failed 'terraform state list' fails the step with "exit ${STUB_EXIT}, calls: $(cat "$STUB_CALLS")" unset STATE_LIST_FAILS TF_STATE_LIST +# The type must start a path segment: an address that only ends in the text +# (a resource NAME like x_aws_db_instance) is not an instance. +export TF_STATE_LIST=$'aws_ssm_parameter.x_aws_db_instance.main\nmodule.m.aws_ssm_parameter.y_aws_db_instance.main' +run_script "$STUB_STATE" +assert_behaviour "behaviour: an address whose segment merely ends in 'aws_db_instance' does not count as an instance" \ + "$([[ "$STUB_EXIT" -eq 0 && ! -s "$STUB_CALLS" ]] && echo 0 || echo 1)" \ + "exit ${STUB_EXIT}, stderr: ${STUB_ERR}, calls: $(cat "$STUB_CALLS")" +unset TF_STATE_LIST + # `jq -er` accepts an empty string, so without its own check this reaches AWS. export TF_OUTPUT_JSON='{"database_instance_identifier":{"value":""}}' run_script "$STUB_STATE" @@ -543,12 +552,15 @@ assert_behaviour "behaviour: the empty-identifier error is distinct and says an "$([[ "$STUB_ERR" == *"will NOT fix this"* && "$STUB_ERR" != *"re-apply this state"* ]] && echo 0 || echo 1)" \ "stderr: ${STUB_ERR}" -# A null value takes the jq branch, not the empty branch. +# A null value takes the jq branch, not the empty branch. The state list is +# instance-free, so a mutant that consults it for null exits 0 instead of 1. +export TF_STATE_LIST=$'aws_ecr_repository.main\nmodule.networking.aws_vpc.main' export TF_OUTPUT_JSON='{"database_instance_identifier":{"value":null}}' run_script "$STUB_STATE" assert_behaviour "behaviour: a null identifier exits 1 without calling aws" \ "$([[ "$STUB_EXIT" -eq 1 && ! -s "$STUB_CALLS" ]] && echo 0 || echo 1)" \ "exit ${STUB_EXIT}, calls: $(cat "$STUB_CALLS")" +unset TF_STATE_LIST # The golden path, against the hostile listing. export TF_OUTPUT_JSON='{"database_instance_identifier":{"value":"cudly-dev-1a2b3c4d-postgres"}}'