Files
tribes-supertest/scripts/cleanup-cloud-resources
self 7f6224d866 test: poll edge certificate SAN validation
Allow ACME certificate repair to converge after DNS and cluster membership updates by polling edge certificate SANs for up to seven minutes.
2026-07-02 15:25:02 +02:00

541 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dry_run=0
all_keys=0
providers="hetzner scaleway ovh"
failures=0
scaleway_zones=(
fr-par-1 fr-par-2 fr-par-3
nl-ams-1 nl-ams-2 nl-ams-3
pl-waw-1 pl-waw-2 pl-waw-3
it-mil-1
)
usage() {
cat <<'USAGE'
Usage: scripts/cleanup-cloud-resources [OPTIONS]
Delete cloud resources and DNS zones from the provider accounts used by supertest.
This does not read, modify, or remove Legion state or supertest artifacts.
Options:
--dry-run Print delete commands without running them.
--provider NAME Limit cleanup to one provider: hetzner, scaleway, ovh.
Can be repeated.
--all-keys Delete all provider SSH keys too. By default only
obvious supertest/Legion test keys are deleted.
-h, --help Show this help.
Examples:
scripts/cleanup-cloud-resources
scripts/cleanup-cloud-resources --dry-run
scripts/cleanup-cloud-resources --provider scaleway
USAGE
}
log() {
printf '%s\n' "$*"
}
warn() {
printf 'warning: %s\n' "$*" >&2
}
fail() {
failures=$((failures + 1))
printf 'error: %s\n' "$*" >&2
}
have_provider() {
local needle="$1"
[[ " $providers " == *" $needle "* ]]
}
have_command() {
command -v "$1" >/dev/null 2>&1
}
is_test_name() {
local name="${1:-}"
[[ "$name" == st-* || "$name" == *tribes-supertest* || "$name" == *legion-supertest* ]]
}
normalize_dns_name() {
local name="${1:-}"
name="${name%.}"
printf '%s' "${name,,}"
}
json_array() {
jq -c 'if type == "array" then . elif . == null then [] else [.] end'
}
run_delete() {
if (( dry_run )); then
printf '+'
printf ' %q' "$@"
printf '\n'
return 0
fi
printf '+'
printf ' %q' "$@"
printf '\n'
"$@"
}
run_ovh_api_post() {
local path="$1"
if (( dry_run )); then
printf '+ ovh-api POST %q\n' "$path"
return 0
fi
printf '+ ovh-api POST %q\n' "$path"
node - "$path" <<'NODE'
const crypto = require("node:crypto")
async function main() {
const path = process.argv[2]
const endpoint = (process.env.OVH_ENDPOINT || "ovh-eu").trim()
const appKey = (process.env.OVH_APPLICATION_KEY || process.env.OVH_APP_KEY || "").trim()
const appSecret = (process.env.OVH_APPLICATION_SECRET || process.env.OVH_APP_SECRET || "").trim()
const consumerKey = (process.env.OVH_CONSUMER_KEY || "").trim()
const host = endpoint === "ovh-ca" ? "ca.api.ovh.com" : "eu.api.ovh.com"
const baseUrl = `https://${host}/1.0`
if (!appKey || !appSecret || !consumerKey) {
throw new Error("OVH application key/secret or consumer key is not set.")
}
const timeResponse = await fetch(`${baseUrl}/auth/time`, {
headers: { "X-Ovh-Application": appKey }
})
if (!timeResponse.ok) {
throw new Error(`OVH auth time failed: ${timeResponse.status} ${await timeResponse.text()}`)
}
const timestamp = (await timeResponse.text()).trim()
const method = "POST"
const body = ""
const url = `${baseUrl}${path}`
const signaturePayload = [appSecret, consumerKey, method, url, body, timestamp].join("+")
const signature = `$1$${crypto.createHash("sha1").update(signaturePayload).digest("hex")}`
const response = await fetch(url, {
method,
headers: {
"X-Ovh-Application": appKey,
"X-Ovh-Consumer": consumerKey,
"X-Ovh-Signature": signature,
"X-Ovh-Timestamp": timestamp
}
})
if (!response.ok) {
throw new Error(`OVH API POST ${path} failed: ${response.status} ${await response.text()}`)
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
})
NODE
}
delete_hcloud_collection() {
local resource="$1"
local label="$2"
local list
if ! list="$(hcloud "$resource" list -o json)"; then
fail "failed to list Hetzner $label"
return
fi
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Hetzner $label: ${name:-$id} ($id)"
run_delete hcloud "$resource" delete "$id" || fail "failed to delete Hetzner $label $id"
done < <(printf '%s\n' "$list" | json_array | jq -r '.[] | [.id, (.name // "")] | @tsv')
}
cleanup_hetzner() {
if [[ -z "${HCLOUD_TOKEN:-}" ]]; then
warn "skipping Hetzner cleanup; HCLOUD_TOKEN is not set"
return
fi
if ! have_command hcloud; then
warn "skipping Hetzner cleanup; hcloud is not in PATH"
return
fi
log "== Hetzner =="
delete_hcloud_collection server "server"
delete_hcloud_collection load-balancer "load balancer"
delete_hcloud_collection volume "volume"
delete_hcloud_collection floating-ip "floating IP"
delete_hcloud_collection primary-ip "primary IP"
delete_hcloud_collection firewall "firewall"
delete_hcloud_collection network "network"
delete_hcloud_collection zone "DNS zone"
local list
if ! list="$(hcloud ssh-key list -o json)"; then
fail "failed to list Hetzner SSH keys"
return
fi
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
if (( all_keys )) || is_test_name "$name"; then
log "Deleting Hetzner SSH key: ${name:-$id} ($id)"
run_delete hcloud ssh-key delete "$id" || fail "failed to delete Hetzner SSH key $id"
else
log "Keeping Hetzner SSH key: ${name:-$id} ($id)"
fi
done < <(printf '%s\n' "$list" | json_array | jq -r '.[] | [.id, (.name // "")] | @tsv')
}
scw_list_zone() {
local zone="$1"
shift
scw "$@" list zone="$zone" -o json 2>/dev/null | json_array
}
cleanup_scaleway_zone() {
local zone="$1"
local list
if list="$(scw_list_zone "$zone" instance server)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway server in $zone: ${name:-$id} ($id)"
run_delete scw instance server delete "$id" with-volumes=all with-ip=true force-shutdown=true zone="$zone" -w ||
fail "failed to delete Scaleway server $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list Scaleway servers in $zone"
fi
if list="$(scw_list_zone "$zone" instance volume)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway instance volume in $zone: ${name:-$id} ($id)"
run_delete scw instance volume delete "$id" zone="$zone" ||
fail "failed to delete Scaleway instance volume $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list Scaleway instance volumes in $zone"
fi
if list="$(scw_list_zone "$zone" block snapshot)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway block snapshot in $zone: ${name:-$id} ($id)"
run_delete scw block snapshot delete "$id" zone="$zone" ||
fail "failed to delete Scaleway block snapshot $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list Scaleway block snapshots in $zone"
fi
if list="$(scw_list_zone "$zone" block volume)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway block volume in $zone: ${name:-$id} ($id)"
run_delete scw block volume delete "$id" zone="$zone" ||
fail "failed to delete Scaleway block volume $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list Scaleway block volumes in $zone"
fi
if list="$(scw_list_zone "$zone" instance ip)"; then
while IFS=$'\t' read -r id address; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway flexible IP in $zone: ${address:-$id} ($id)"
run_delete scw instance ip delete "$id" zone="$zone" ||
fail "failed to delete Scaleway flexible IP $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.address // .ip // "")] | @tsv')
else
warn "failed to list Scaleway flexible IPs in $zone"
fi
if list="$(scw_list_zone "$zone" instance placement-group)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway placement group in $zone: ${name:-$id} ($id)"
run_delete scw instance placement-group delete "$id" zone="$zone" ||
fail "failed to delete Scaleway placement group $id in $zone"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list Scaleway placement groups in $zone"
fi
if list="$(scw_list_zone "$zone" instance security-group)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting Scaleway security group in $zone: ${name:-$id} ($id)"
run_delete scw instance security-group delete "$id" zone="$zone" ||
fail "failed to delete Scaleway security group $id in $zone"
done < <(
printf '%s\n' "$list" |
jq -r '.[] |
select((.organization_default // false | not) and
(.project_default // false | not) and
((.name // "") != "Default security group")) |
[.id, (.name // "")] | @tsv'
)
else
warn "failed to list Scaleway security groups in $zone"
fi
}
cleanup_scaleway_dns_zones() {
local domain
domain="$(normalize_dns_name "${SUPERTEST_DNS_DOMAIN:-}")"
if [[ -z "$domain" ]]; then
warn "skipping Scaleway DNS zone cleanup; Scaleway external-domain API has no list command and SUPERTEST_DNS_DOMAIN is not set"
return
fi
log "Trying to delete Scaleway external DNS zone: $domain"
if (( dry_run )); then
run_delete scw domain external-domain delete domain="$domain"
return
fi
local output
if output="$(run_delete scw domain external-domain delete domain="$domain" 2>&1)"; then
printf '%s\n' "$output"
return
fi
if grep -qi 'domain not found' <<<"$output"; then
log "Scaleway external DNS zone not found: $domain"
return
fi
printf '%s\n' "$output" >&2
fail "failed to delete Scaleway external DNS zone $domain"
}
cleanup_scaleway() {
if [[ -z "${SCW_ACCESS_KEY:-}" || -z "${SCW_SECRET_KEY:-}" || -z "${SCW_DEFAULT_PROJECT_ID:-}" ]]; then
warn "skipping Scaleway cleanup; SCW_ACCESS_KEY, SCW_SECRET_KEY, or SCW_DEFAULT_PROJECT_ID is not set"
return
fi
if ! have_command scw; then
warn "skipping Scaleway cleanup; scw is not in PATH"
return
fi
log "== Scaleway =="
for zone in "${scaleway_zones[@]}"; do
cleanup_scaleway_zone "$zone"
done
cleanup_scaleway_dns_zones
}
ovh_project_ids() {
ovhcloud cloud project list -o json | json_array | jq -r '.[] | .project_id // .projectId // .id // empty'
}
ovh_list_project() {
local project="$1"
shift
ovhcloud cloud "$@" list --cloud-project "$project" -o json 2>/dev/null | json_array
}
cleanup_ovh_project() {
local project="$1"
local list
log "OVH project $project"
if list="$(ovh_list_project "$project" instance)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting OVH instance: ${name:-$id} ($id)"
run_delete ovhcloud cloud instance delete "$id" --cloud-project "$project" ||
fail "failed to delete OVH instance $id"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list OVH instances for project $project"
fi
if list="$(ovh_list_project "$project" loadbalancer)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting OVH load balancer: ${name:-$id} ($id)"
run_delete ovhcloud cloud loadbalancer delete "$id" --cloud-project "$project" ||
fail "failed to delete OVH load balancer $id"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list OVH load balancers for project $project"
fi
if list="$(ovh_list_project "$project" instance snapshot)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting OVH instance snapshot: ${name:-$id} ($id)"
run_delete ovhcloud cloud instance snapshot delete "$id" --cloud-project "$project" ||
fail "failed to delete OVH instance snapshot $id"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list OVH instance snapshots for project $project"
fi
if list="$(ovh_list_project "$project" storage block)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
log "Deleting OVH block volume: ${name:-$id} ($id)"
run_delete ovhcloud cloud storage block delete "$id" --cloud-project "$project" ||
fail "failed to delete OVH block volume $id"
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list OVH block volumes for project $project"
fi
if list="$(ovh_list_project "$project" network private)"; then
while IFS=$'\t' read -r id name region; do
[[ -n "${id:-}" ]] || continue
if is_test_name "$name"; then
log "Deleting OVH private network: ${name:-$id} ($id)"
run_delete ovhcloud cloud network private delete "$id" --cloud-project "$project" --region "$region" ||
fail "failed to delete OVH private network $id"
else
log "Keeping OVH private network: ${name:-$id} ($id)"
fi
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // ""), (.region // "")] | @tsv')
else
warn "failed to list OVH private networks for project $project"
fi
if list="$(ovh_list_project "$project" ssh-key)"; then
while IFS=$'\t' read -r id name; do
[[ -n "${id:-}" ]] || continue
if (( all_keys )) || is_test_name "$name"; then
log "Deleting OVH SSH key: ${name:-$id} ($id)"
run_delete ovhcloud cloud ssh-key delete "$id" --cloud-project "$project" ||
fail "failed to delete OVH SSH key $id"
else
log "Keeping OVH SSH key: ${name:-$id} ($id)"
fi
done < <(printf '%s\n' "$list" | jq -r '.[] | [.id, (.name // "")] | @tsv')
else
warn "failed to list OVH SSH keys for project $project"
fi
}
cleanup_ovh_dns_zones() {
local list
if ! list="$(ovhcloud domain-zone list -o json 2>/dev/null | json_array)"; then
fail "failed to list OVH DNS zones"
return
fi
while IFS= read -r zone; do
[[ -n "$zone" ]] || continue
local encoded_zone
encoded_zone="$(jq -rn --arg value "$zone" '$value | @uri')"
log "Requesting OVH DNS zone termination: $zone"
run_ovh_api_post "/domain/zone/${encoded_zone}/terminate" ||
fail "failed to request OVH DNS zone termination for $zone"
log "OVH requires email or Manager confirmation before DNS zone $zone is permanently deleted."
done < <(printf '%s\n' "$list" | jq -r '.[] | .name // .zoneName // .id // empty')
}
cleanup_ovh() {
if [[ -z "${OVH_APP_KEY:-}${OVH_APPLICATION_KEY:-}" ||
-z "${OVH_APP_SECRET:-}${OVH_APPLICATION_SECRET:-}" ||
-z "${OVH_CONSUMER_KEY:-}" ]]; then
warn "skipping OVH cleanup; OVH app key/secret or OVH_CONSUMER_KEY is not set"
return
fi
if ! have_command ovhcloud; then
warn "skipping OVH cleanup; ovhcloud is not in PATH"
return
fi
log "== OVH =="
local projects
if ! projects="$(ovh_project_ids)"; then
fail "failed to list OVH cloud projects"
return
fi
while IFS= read -r project; do
[[ -n "$project" ]] || continue
cleanup_ovh_project "$project"
done <<<"$projects"
cleanup_ovh_dns_zones
}
selected_providers=()
while (($#)); do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--provider)
[[ $# -ge 2 ]] || { printf '%s\n' "--provider needs a value" >&2; exit 2; }
selected_providers+=("$2")
shift 2
;;
--provider=*)
selected_providers+=("${1#--provider=}")
shift
;;
--all-keys)
all_keys=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
printf 'Unknown option: %s\n\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
if ((${#selected_providers[@]})); then
providers="${selected_providers[*]}"
fi
for provider in $providers; do
case "$provider" in
hetzner|scaleway|ovh) ;;
*)
printf 'Unknown provider: %s\n' "$provider" >&2
exit 2
;;
esac
done
cd "$repo_root"
if (( dry_run )); then
log "dry run: no resources will be deleted"
fi
have_provider hetzner && cleanup_hetzner
have_provider scaleway && cleanup_scaleway
have_provider ovh && cleanup_ovh
if (( failures > 0 )); then
printf 'cloud cleanup completed with %d failure(s)\n' "$failures" >&2
exit 1
fi
log "cloud cleanup completed"