Skip to content

Commit fc4be17

Browse files
[patch] Retry hosted provider cleanup
1 parent e0e054d commit fc4be17

5 files changed

Lines changed: 1077 additions & 59 deletions

File tree

.github/workflows/cloud-smoke.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ jobs:
3131
run: make terraform-lint-check
3232

3333
- name: Validate fallback cleanup behavior
34-
run: bash ci/cloud-smoke-cleanup-contract.sh
34+
run: |
35+
bash ci/cloud-smoke-cleanup-contract.sh
36+
make hosted-cleanup-retry-contract
3537
3638
config-management-lint:
3739
name: Ansible and Salt lint

Makefile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: lint lint-check shell-lint config-management-smoke
1+
.PHONY: lint lint-check shell-lint hosted-cleanup-retry-contract config-management-smoke
22
.PHONY: terraform-fmt terraform-fmt-check terraform-validate terraform-lint-check terraform-docs
33
.PHONY: config-management-cloud-smoke config-management-cloud-smoke-ansible-drupal config-management-cloud-smoke-salt-drupal
44
.PHONY: destroy-config-management-cloud-smoke destroy-config-management-cloud-smoke-ansible-drupal destroy-config-management-cloud-smoke-salt-drupal
@@ -10,9 +10,9 @@ DOCS_IMAGE ?= cloud-compose-docs
1010
DOCS_PORT ?= 8888
1111
DOCS_DOCKER_USER ?= $(shell id -u):$(shell id -g)
1212

13-
lint: terraform-fmt shell-lint terraform-validate
13+
lint: terraform-fmt shell-lint hosted-cleanup-retry-contract terraform-validate
1414

15-
lint-check: terraform-fmt-check shell-lint terraform-validate
15+
lint-check: terraform-fmt-check shell-lint hosted-cleanup-retry-contract terraform-validate
1616

1717
terraform-fmt:
1818
terraform fmt -recursive
@@ -31,6 +31,9 @@ shell-lint:
3131
-path "./docs/site" -prune -o \
3232
-type f -name "*.sh" -print0 | xargs -0 shellcheck
3333

34+
hosted-cleanup-retry-contract:
35+
bash ci/hosted-cleanup-retry-contract.sh
36+
3437
config-management-smoke:
3538
ci/config-management-smoke.sh
3639

ci/cloud-smoke.sh

Lines changed: 188 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,18 @@ api_request() {
165165
;;
166166
esac
167167

168-
if response="$(curl -sS -X "$method" -H "Authorization: Bearer ${token}" -w $'\n%{http_code}' "${base_url}${path}")"; then
168+
if response="$(curl -sS \
169+
--connect-timeout 10 \
170+
--max-time 45 \
171+
-X "$method" \
172+
-H "Authorization: Bearer ${token}" \
173+
-w $'\n%{http_code}' \
174+
"${base_url}${path}")"; then
169175
http_code="${response##*$'\n'}"
170176
body="${response%$'\n'"$http_code"}"
171177
else
172178
echo "${provider} API request failed for ${method} ${path}; check ${token_name} and network access." >&2
173-
return 1
179+
return 75
174180
fi
175181

176182
case "$http_code" in
@@ -185,6 +191,37 @@ api_request() {
185191
echo "${provider} API rejected ${token_name} with HTTP 403 for ${method} ${path}; verify the token has the permissions required by smoke cleanup and Terraform." >&2
186192
return 22
187193
;;
194+
404 | 410)
195+
if [[ "$method" == "DELETE" ]]; then
196+
return 0
197+
fi
198+
echo "${provider} API returned HTTP ${http_code} for ${method} ${path}." >&2
199+
if [[ -n "$body" ]]; then
200+
printf '%s\n' "$body" >&2
201+
fi
202+
return 22
203+
;;
204+
408 | 425 | 429 | 5??)
205+
echo "${provider} API returned retryable HTTP ${http_code} for ${method} ${path}." >&2
206+
if [[ -n "$body" ]]; then
207+
printf '%s\n' "$body" >&2
208+
fi
209+
return 75
210+
;;
211+
409 | 423)
212+
if [[ "$method" == "DELETE" ]]; then
213+
echo "${provider} API returned retryable HTTP ${http_code} for ${method} ${path}." >&2
214+
if [[ -n "$body" ]]; then
215+
printf '%s\n' "$body" >&2
216+
fi
217+
return 75
218+
fi
219+
echo "${provider} API returned HTTP ${http_code} for ${method} ${path}." >&2
220+
if [[ -n "$body" ]]; then
221+
printf '%s\n' "$body" >&2
222+
fi
223+
return 22
224+
;;
188225
*)
189226
echo "${provider} API returned HTTP ${http_code} for ${method} ${path}." >&2
190227
if [[ -n "$body" ]]; then
@@ -200,7 +237,25 @@ api_delete() {
200237
}
201238

202239
api_get() {
203-
api_request "$1" GET "$2"
240+
local provider="$1" path="$2" attempt=1 delay=2 status
241+
242+
while true; do
243+
if api_request "$provider" GET "$path"; then
244+
return 0
245+
else
246+
status=$?
247+
fi
248+
if [[ "$status" -ne 75 || "$attempt" -ge 6 ]]; then
249+
return "$status"
250+
fi
251+
echo "Retrying ${provider} API GET ${path} in ${delay}s (attempt $((attempt + 1)) of 6)" >&2
252+
sleep "$delay"
253+
attempt=$((attempt + 1))
254+
delay=$((delay * 2))
255+
if [[ "$delay" -gt 30 ]]; then
256+
delay=30
257+
fi
258+
done
204259
}
205260

206261
gcp_region() {
@@ -212,7 +267,7 @@ gcp_zone() {
212267
}
213268

214269
delete_ids() {
215-
local provider="$1" path_prefix="$2" id attempt
270+
local provider="$1" path_prefix="$2" id attempt status
216271
local failed=0 deleted
217272

218273
while IFS= read -r id; do
@@ -225,11 +280,18 @@ delete_ids() {
225280
if api_delete "$provider" "${path_prefix}/${id}"; then
226281
deleted=true
227282
break
283+
else
284+
status=$?
285+
fi
286+
if [[ "$status" -ne 75 ]]; then
287+
break
288+
fi
289+
if [[ "$attempt" -lt 12 ]]; then
290+
sleep 10
228291
fi
229-
sleep 10
230292
done
231293
if [[ "$deleted" != "true" ]]; then
232-
echo "Failed to delete ${provider} ${path_prefix}/${id} after 12 attempts" >&2
294+
echo "Failed to delete ${provider} ${path_prefix}/${id} after ${attempt} attempt(s)" >&2
233295
failed=1
234296
fi
235297
done
@@ -453,9 +515,9 @@ target_name_prefix() {
453515
printf 'cc-%s-%s\n' "$(provider_slug "$provider")" "$(template_slug "$template")"
454516
}
455517

456-
provider_tag_cleanup() {
457-
local target="$1" run_id="${2:-}" run_tag run_fragment provider name_prefix
458-
local cleanup_status=0
518+
provider_resource_ids() {
519+
local target="$1" run_id="${2:-}" kind="$3"
520+
local run_tag run_fragment provider name_prefix
459521

460522
run_tag="$(smoke_run_tag "$run_id")"
461523
run_fragment=""
@@ -465,30 +527,107 @@ provider_tag_cleanup() {
465527
provider="$(target_provider "$target")"
466528
name_prefix="$(target_name_prefix "$target")"
467529

468-
case "$provider" in
469-
digitalocean)
530+
case "${provider}:${kind}" in
531+
digitalocean:firewalls)
470532
api_get digitalocean "/firewalls?per_page=200" |
471-
jq -r --arg name_prefix "${name_prefix}-" --arg run_fragment "$run_fragment" '.firewalls[]? | select(.name | startswith($name_prefix)) | select($run_fragment == "" or (.name | contains($run_fragment))) | .id' |
472-
delete_ids digitalocean "/firewalls" || cleanup_status=1
533+
jq -r --arg name_prefix "${name_prefix}-" --arg run_fragment "$run_fragment" 'if (.firewalls | type) != "array" then error("DigitalOcean firewalls response is not an array") else .firewalls[] | select(.name | startswith($name_prefix)) | select($run_fragment == "" or (.name | contains($run_fragment))) | .id end'
534+
;;
535+
digitalocean:droplets)
473536
api_get digitalocean "/droplets?tag_name=cloud-compose-smoke&per_page=200" |
474-
jq -r --arg target "$target" --arg run_tag "$run_tag" '.droplets[]? | select((.tags // []) | index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id' |
475-
delete_ids digitalocean "/droplets" || cleanup_status=1
476-
sleep 10
537+
jq -r --arg target "$target" --arg run_tag "$run_tag" 'if (.droplets | type) != "array" then error("DigitalOcean droplets response is not an array") else .droplets[] | select((.tags // []) | index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id end'
538+
;;
539+
digitalocean:volumes)
477540
api_get digitalocean "/volumes?tag_name=cloud-compose-smoke&per_page=200" |
478-
jq -r --arg target "$target" --arg run_tag "$run_tag" '.volumes[]? | select((.tags // []) | index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id' |
479-
delete_ids digitalocean "/volumes" || cleanup_status=1
541+
jq -r --arg target "$target" --arg run_tag "$run_tag" 'if (.volumes | type) != "array" then error("DigitalOcean volumes response is not an array") else .volumes[] | select((.tags // []) | index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id end'
480542
;;
481-
linode)
543+
linode:firewalls)
482544
api_get linode "/networking/firewalls?page_size=500" |
483-
jq -r --arg target "$target" --arg run_tag "$run_tag" '.data[]? | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id' |
484-
delete_ids linode "/networking/firewalls" || cleanup_status=1
545+
jq -r --arg target "$target" --arg run_tag "$run_tag" 'if (.data | type) != "array" then error("Linode firewalls response data is not an array") else .data[] | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id end'
546+
;;
547+
linode:instances)
485548
api_get linode "/linode/instances?page_size=500" |
486-
jq -r --arg target "$target" --arg run_tag "$run_tag" '.data[]? | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id' |
487-
delete_ids linode "/linode/instances" || cleanup_status=1
488-
sleep 10
549+
jq -r --arg target "$target" --arg run_tag "$run_tag" 'if (.data | type) != "array" then error("Linode instances response data is not an array") else .data[] | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id end'
550+
;;
551+
linode:volumes)
489552
api_get linode "/volumes?page_size=500" |
490-
jq -r --arg target "$target" --arg run_tag "$run_tag" '.data[]? | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id' |
491-
delete_ids linode "/volumes" || cleanup_status=1
553+
jq -r --arg target "$target" --arg run_tag "$run_tag" 'if (.data | type) != "array" then error("Linode volumes response data is not an array") else .data[] | select((.tags // []) | index("cloud-compose-smoke") and index($target)) | select($run_tag == "" or ((.tags // []) | index($run_tag))) | .id end'
554+
;;
555+
*)
556+
echo "Unknown provider resource kind: ${provider}:${kind}" >&2
557+
return 2
558+
;;
559+
esac
560+
}
561+
562+
provider_cleanup_residuals() {
563+
local target="$1" run_id="${2:-}" provider kind ids id index
564+
local -a kinds path_prefixes
565+
566+
provider="$(target_provider "$target")"
567+
case "$provider" in
568+
digitalocean)
569+
kinds=(firewalls droplets volumes)
570+
path_prefixes=(/firewalls /droplets /volumes)
571+
;;
572+
linode)
573+
kinds=(firewalls instances volumes)
574+
path_prefixes=(/networking/firewalls /linode/instances /volumes)
575+
;;
576+
*)
577+
return 0
578+
;;
579+
esac
580+
581+
for index in "${!kinds[@]}"; do
582+
kind="${kinds[$index]}"
583+
if ! ids="$(provider_resource_ids "$target" "$run_id" "$kind")"; then
584+
return 1
585+
fi
586+
while IFS= read -r id; do
587+
[[ -n "$id" ]] || continue
588+
printf '%s%s\n' "${path_prefixes[$index]}/" "$id"
589+
done <<<"$ids"
590+
done
591+
}
592+
593+
verify_no_provider_resources() {
594+
local target="$1" run_id="${2:-}" attempt residuals
595+
596+
for attempt in {1..6}; do
597+
if ! residuals="$(provider_cleanup_residuals "$target" "$run_id")"; then
598+
echo "Could not verify provider cleanup for ${target}" >&2
599+
return 1
600+
fi
601+
if [[ -z "$residuals" ]]; then
602+
echo "Verified that no matching ${target} smoke resources remain"
603+
return 0
604+
fi
605+
echo "Matching ${target} smoke resources remain after cleanup verification attempt ${attempt}:" >&2
606+
printf '%s\n' "$residuals" >&2
607+
if [[ "$attempt" -lt 6 ]]; then
608+
sleep 10
609+
fi
610+
done
611+
612+
echo "Provider cleanup left matching ${target} smoke resources" >&2
613+
return 1
614+
}
615+
616+
provider_tag_cleanup() {
617+
local target="$1" run_id="${2:-}" provider kind index name_prefix
618+
local cleanup_status=0
619+
local -a kinds path_prefixes
620+
621+
provider="$(target_provider "$target")"
622+
name_prefix="$(target_name_prefix "$target")"
623+
case "$provider" in
624+
digitalocean)
625+
kinds=(firewalls droplets volumes)
626+
path_prefixes=(/firewalls /droplets /volumes)
627+
;;
628+
linode)
629+
kinds=(firewalls instances volumes)
630+
path_prefixes=(/networking/firewalls /linode/instances /volumes)
492631
;;
493632
gcp)
494633
local project name_filter region cloud_run_services instance_rows firewall_names disk_rows
@@ -674,6 +813,20 @@ provider_tag_cleanup() {
674813
;;
675814
esac
676815

816+
if [[ "$provider" == "digitalocean" || "$provider" == "linode" ]]; then
817+
for index in "${!kinds[@]}"; do
818+
kind="${kinds[$index]}"
819+
provider_resource_ids "$target" "$run_id" "$kind" |
820+
delete_ids "$provider" "${path_prefixes[$index]}" || cleanup_status=1
821+
if [[ "$kind" == "droplets" || "$kind" == "instances" ]]; then
822+
sleep 10
823+
fi
824+
done
825+
if [[ "$cleanup_status" -eq 0 ]] && ! verify_no_provider_resources "$target" "$run_id"; then
826+
cleanup_status=1
827+
fi
828+
fi
829+
677830
return "$cleanup_status"
678831
}
679832

@@ -967,7 +1120,7 @@ run_target() (
9671120
set -euo pipefail
9681121

9691122
local target="$1"
970-
local root workdir key_path home_dir output_json public_key
1123+
local root workdir key_path home_dir output_json public_key run_id
9711124
local -a auto_args var_args
9721125

9731126
root="$(target_root "$target")"
@@ -981,6 +1134,7 @@ run_target() (
9811134
chmod 0700 "$home_dir/.ssh"
9821135

9831136
ensure_key "$key_path"
1137+
run_id="$(smoke_run_id)"
9841138
mapfile -d '' -t var_args < <(target_var_args "$root" "$key_path" "$target")
9851139

9861140
auto_args=()
@@ -991,7 +1145,7 @@ run_target() (
9911145
cleanup_started=false
9921146
# shellcheck disable=SC2317
9931147
cleanup() {
994-
local status=$?
1148+
local status="$1"
9951149
local destroy_status destroy_timeout cleanup_status
9961150

9971151
trap - EXIT INT TERM HUP
@@ -1020,14 +1174,18 @@ run_target() (
10201174
fi
10211175
if [[ "$destroy_status" -ne 0 ]]; then
10221176
echo "Terraform destroy failed for ${target}; attempting provider tag cleanup"
1023-
provider_tag_cleanup "$target" || cleanup_status=$?
1177+
provider_tag_cleanup "$target" "$run_id" || cleanup_status=$?
10241178
fi
10251179
if [[ "$status" -eq 0 && "$destroy_status" -ne 0 && "$cleanup_status" -ne 0 ]]; then
1180+
echo "Terraform destroy and provider tag cleanup both failed for ${target}" >&2
10261181
exit "$destroy_status"
10271182
fi
10281183
exit "$status"
10291184
}
1030-
trap cleanup EXIT INT TERM HUP
1185+
trap 'cleanup "$?"' EXIT
1186+
trap 'exit 129' HUP
1187+
trap 'exit 130' INT
1188+
trap 'exit 143' TERM
10311189

10321190
echo "Initializing ${target}"
10331191
TF_DATA_DIR="$workdir/.terraform" terraform -chdir="$root" init -input=false

0 commit comments

Comments
 (0)