diff --git a/scripts/cleanup-cloud-resources b/scripts/cleanup-cloud-resources index 07e3640..83b0cf7 100755 --- a/scripts/cleanup-cloud-resources +++ b/scripts/cleanup-cloud-resources @@ -19,7 +19,7 @@ usage() { cat <<'USAGE' Usage: scripts/cleanup-cloud-resources [OPTIONS] -Delete cloud resources from the provider accounts used by supertest. +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: @@ -64,6 +64,12 @@ is_test_name() { [[ "$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' } @@ -82,6 +88,64 @@ run_delete() { "$@" } +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" @@ -117,6 +181,7 @@ cleanup_hetzner() { 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 @@ -229,6 +294,36 @@ cleanup_scaleway_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 "Deleting 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" @@ -243,6 +338,7 @@ cleanup_scaleway() { for zone in "${scaleway_zones[@]}"; do cleanup_scaleway_zone "$zone" done + cleanup_scaleway_dns_zones } ovh_project_ids() { @@ -294,11 +390,11 @@ cleanup_ovh_project() { warn "failed to list OVH instance snapshots for project $project" fi - if list="$(ovh_list_project "$project" storage-block)"; then + 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" || + 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 @@ -336,6 +432,25 @@ cleanup_ovh_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:-}" || @@ -358,6 +473,7 @@ cleanup_ovh() { [[ -n "$project" ]] || continue cleanup_ovh_project "$project" done <<<"$projects" + cleanup_ovh_dns_zones } selected_providers=()