@@ -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
202239api_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
206261gcp_region () {
@@ -212,7 +267,7 @@ gcp_zone() {
212267}
213268
214269delete_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