diff --git a/scripts/front-door/ssl-certificate-regeneration.sh b/scripts/front-door/ssl-certificate-regeneration.sh index 1f711e75..a7e53f15 100644 --- a/scripts/front-door/ssl-certificate-regeneration.sh +++ b/scripts/front-door/ssl-certificate-regeneration.sh @@ -4,8 +4,12 @@ # Azure Front Door SSL Certificate Renewal - Process Flow ################################################################################ # -# 1. Receive the Azure subscription, target domain and certificate expiry -# threshold as command-line arguments. +# 1. Receive the Azure subscription, one or more target domains and the +# certificate expiry threshold as command-line arguments. +# +# Steps 2-9 are then run independently for each target domain. A failure +# for one domain does not stop the remaining domains being processed; the +# script exits non-zero at the end if any domain failed. # # 2. Find the Azure Front Door profile containing the requested custom domain. # @@ -67,11 +71,11 @@ TODAY=$(TZ=Europe/London date -Idate) ################################################################################ usage() { - echo "Usage: $0 -s -d -e [-r ]" + echo "Usage: $0 -s -d [-d ...] -e [-r ]" echo echo "Required arguments:" echo " -s Azure subscription name or ID" - echo " -d Target domain" + echo " -d Target domain. Repeat -d, or pass a comma-separated list, to process multiple domains" echo " -e TLS certificate expiration threshold in days" echo echo "Optional arguments:" @@ -81,11 +85,13 @@ usage() { echo "Example:" echo " $0 -s my-subscription -d example.nhs.uk -e 30" echo " $0 -s my-subscription -d example.nhs.uk -e 30 -f" + echo " $0 -s my-subscription -d example.nhs.uk -d www.example.nhs.uk -e 30" + echo " $0 -s my-subscription -d example.nhs.uk,www.example.nhs.uk -e 30" exit 1 } AZ_SUBSCRIPTION_SCOPE="" -TARGET_DOMAIN="" +TARGET_DOMAINS=() AFD_DOMAIN_EXPIRATION_DAYS="" RESOURCE_GROUP_FILTER="" FORCE_REGENERATION=false @@ -96,7 +102,13 @@ while getopts ":s:d:e:fr:h" opt; do AZ_SUBSCRIPTION_SCOPE="$OPTARG" ;; d) - TARGET_DOMAIN="$OPTARG" + IFS=',' read -r -a DOMAIN_ARGS <<< "$OPTARG" + for DOMAIN_ARG in "${DOMAIN_ARGS[@]}"; do + DOMAIN_ARG="${DOMAIN_ARG//[[:space:]]/}" + if [[ -n "$DOMAIN_ARG" ]]; then + TARGET_DOMAINS+=("$DOMAIN_ARG") + fi + done ;; e) AFD_DOMAIN_EXPIRATION_DAYS="$OPTARG" @@ -134,7 +146,7 @@ if [[ -z "$AZ_SUBSCRIPTION_SCOPE" ]]; then usage fi -if [[ -z "$TARGET_DOMAIN" ]]; then +if [[ "${#TARGET_DOMAINS[@]}" -eq 0 ]]; then echo "ERROR: Target domain is required. Use -d ." >&2 usage fi @@ -151,7 +163,7 @@ if ! [[ "$AFD_DOMAIN_EXPIRATION_DAYS" =~ ^[0-9]+$ ]]; then fi echo "TLS certificate expiration threshold: $AFD_DOMAIN_EXPIRATION_DAYS days" -echo "Target domain: $TARGET_DOMAIN" +echo "Target domain(s): ${TARGET_DOMAINS[*]}" echo "Azure subscription: $AZ_SUBSCRIPTION_SCOPE" if [[ -n "$RESOURCE_GROUP_FILTER" ]]; then @@ -176,9 +188,15 @@ AFD_LIST=$( }' ) +if [[ -z "$AFD_LIST" ]]; then + echo "ERROR: No Azure Front Door (Standard/Premium) profiles found in subscription $AZ_SUBSCRIPTION_SCOPE" >&2 + exit 1 +fi + AFD_COUNT=$(echo "$AFD_LIST" | wc -l | tr -d ' ') -echo "Found $AFD_COUNT Azure Front Door(s) total" +echo "Found $AFD_COUNT Azure Front Door(s) total:" +echo "$AFD_LIST" | jq -r '" - \(.name) (Resource Group: \(.resourceGroup))"' echo if [[ -n "$RESOURCE_GROUP_FILTER" ]]; then @@ -187,544 +205,609 @@ if [[ -n "$RESOURCE_GROUP_FILTER" ]]; then fi ################################################################################ -# Find the Azure Front Door containing TARGET_DOMAIN +# Process a single domain +# +# Runs the full renewal/revalidation flow for the domain passed as $1. +# Called in a subshell per domain, so any "exit" only ends that domain's run. ################################################################################ -TARGET_AFD="" -TARGET_RESOURCE_GROUP="" -MATCHING_DOMAIN="" - -while IFS= read -r AZURE_FRONT_DOOR; do +process_domain() { + + local target_domain="$1" + + # Declared separately from assignment so "local" doesn't mask the exit status + # of command substitutions (checked via $? and "if !" below). + local azure_front_door afd_name all_custom_domains matching_domain \ + target_afd target_resource_group resource_group \ + domain domain_name resource_id state provisioning_state certificate_type \ + domain_validation_expiry domain_token \ + domain_dns_zone domain_dns_zone_id domain_dns_zone_name domain_dns_zone_rg \ + ssl_timeout_seconds cert_end_date cert_expiry_seconds today_seconds \ + cert_days_until_expiry cert_expiration_threshold_seconds should_regenerate \ + regenerate_result wait_for_pending_seconds wait_for_approved_seconds \ + wait_interval_seconds waited_seconds \ + record_set_name_tmp record_set_name dns_result dns_exit_code record_exists \ + record_set_state record_set_current_token dns_update_result \ + current_domain current_state current_provisioning_state - RESOURCE_GROUP=$(echo "$AZURE_FRONT_DOOR" | jq -rc '.resourceGroup') - AFD_NAME=$(echo "$AZURE_FRONT_DOOR" | jq -rc '.name') + ################################################################################ + # Find the Azure Front Door containing target_domain + ################################################################################ - # Skip if resource group filter is set and doesn't match. - if [[ -n "$RESOURCE_GROUP_FILTER" ]] && [[ "$RESOURCE_GROUP" != "$RESOURCE_GROUP_FILTER" ]]; then - continue - fi + target_afd="" + target_resource_group="" + matching_domain="" - echo "Checking Front Door $AFD_NAME in Resource Group $RESOURCE_GROUP..." + while IFS= read -r azure_front_door; do - ALL_CUSTOM_DOMAINS=$( - az afd custom-domain list \ - --profile-name "$AFD_NAME" \ - --output json \ - --only-show-errors \ - --subscription "$AZ_SUBSCRIPTION_SCOPE" \ - --resource-group "$RESOURCE_GROUP" - ) + [[ -z "$azure_front_door" ]] && continue - MATCHING_DOMAIN=$( - echo "$ALL_CUSTOM_DOMAINS" | - jq -c --arg TARGET_DOMAIN "$TARGET_DOMAIN" ' - .[] | - select(.hostName == $TARGET_DOMAIN) | - { - "domain": .hostName, - "id": .id, - "validationProperties": .validationProperties, - "state": .domainValidationState, - "provisioningState": .provisioningState, - "certificateType": .tlsSettings.certificateType, - "azureDnsZone": .azureDnsZone - } - ' - ) + resource_group=$(echo "$azure_front_door" | jq -rc '.resourceGroup') + afd_name=$(echo "$azure_front_door" | jq -rc '.name') - if [[ -n "$MATCHING_DOMAIN" ]]; then + # Skip if resource group filter is set and doesn't match. + if [[ -n "$RESOURCE_GROUP_FILTER" ]] && [[ "$resource_group" != "$RESOURCE_GROUP_FILTER" ]]; then + continue + fi - TARGET_AFD="$AFD_NAME" - TARGET_RESOURCE_GROUP="$RESOURCE_GROUP" + echo "Checking Front Door $afd_name in Resource Group $resource_group..." - break + all_custom_domains=$( + az afd custom-domain list \ + --profile-name "$afd_name" \ + --output json \ + --only-show-errors \ + --subscription "$AZ_SUBSCRIPTION_SCOPE" \ + --resource-group "$resource_group" + ) - fi + matching_domain=$( + echo "$all_custom_domains" | + jq -c --arg TARGET_DOMAIN "$target_domain" ' + .[] | + select(.hostName == $TARGET_DOMAIN) | + { + "domain": .hostName, + "id": .id, + "validationProperties": .validationProperties, + "state": .domainValidationState, + "provisioningState": .provisioningState, + "certificateType": .tlsSettings.certificateType, + "azureDnsZone": .azureDnsZone + } + ' + ) -done <<< "$AFD_LIST" + if [[ -n "$matching_domain" ]]; then -################################################################################ -# Make sure target domain was found -################################################################################ + target_afd="$afd_name" + target_resource_group="$resource_group" -if [[ -z "$MATCHING_DOMAIN" ]]; then + break - echo - echo "ERROR: Target domain was not found:" >&2 - echo " $TARGET_DOMAIN" - echo - echo "Make sure the domain exists in the selected subscription." - exit 1 + fi -fi + done <<< "$AFD_LIST" -################################################################################ -# Extract domain properties -################################################################################ + ################################################################################ + # Make sure target domain was found + ################################################################################ -DOMAIN="$MATCHING_DOMAIN" + if [[ -z "$matching_domain" ]]; then -DOMAIN_NAME=$(echo "$DOMAIN" | jq -rc '.domain') -RESOURCE_ID=$(echo "$DOMAIN" | jq -rc '.id') -STATE=$(echo "$DOMAIN" | jq -rc '.state') -PROVISIONING_STATE=$(echo "$DOMAIN" | jq -rc '.provisioningState') -CERTIFICATE_TYPE=$(echo "$DOMAIN" | jq -rc '.certificateType') + echo + echo "ERROR: Target domain was not found:" >&2 + echo " $target_domain" + echo + echo "Make sure the domain exists in the selected subscription." + exit 1 -DOMAIN_VALIDATION_EXPIRY=$( - echo "$DOMAIN" | - jq -rc '.validationProperties.expirationDate' -) + fi -DOMAIN_TOKEN=$( - echo "$DOMAIN" | - jq -rc '.validationProperties.validationToken' -) + ################################################################################ + # Extract domain properties + ################################################################################ -DOMAIN_DNS_ZONE_ID=$( - echo "$DOMAIN" | - jq -rc '.azureDnsZone.id' -) + domain="$matching_domain" -echo -echo "============================================================" -echo "Processing ONLY the requested domain" -echo "============================================================" -echo "Front Door: $TARGET_AFD" -echo "Resource Group: $TARGET_RESOURCE_GROUP" -echo "Domain: $DOMAIN_NAME" -echo "Certificate Type: $CERTIFICATE_TYPE" -echo "Resource ID: $RESOURCE_ID" -echo "Provisioning state: $PROVISIONING_STATE" -echo "Validation state: $STATE" -echo "============================================================" -echo + domain_name=$(echo "$domain" | jq -rc '.domain') + resource_id=$(echo "$domain" | jq -rc '.id') + state=$(echo "$domain" | jq -rc '.state') + provisioning_state=$(echo "$domain" | jq -rc '.provisioningState') + certificate_type=$(echo "$domain" | jq -rc '.certificateType') -################################################################################ -# Get currently served TLS certificate expiry -################################################################################ + domain_validation_expiry=$( + echo "$domain" | + jq -rc '.validationProperties.expirationDate' + ) -echo "Checking currently served TLS certificate..." + domain_token=$( + echo "$domain" | + jq -rc '.validationProperties.validationToken' + ) -SSL_TIMEOUT_SECONDS=60 + domain_dns_zone_id=$( + echo "$domain" | + jq -rc '.azureDnsZone.id' + ) -CERT_END_DATE=$( - echo | timeout "${SSL_TIMEOUT_SECONDS}s" openssl s_client \ - -connect "${DOMAIN_NAME}:443" \ - -servername "${DOMAIN_NAME}" \ - 2>/dev/null | - openssl x509 -noout -enddate | - cut -d= -f2 -) + echo + echo "============================================================" + echo "Processing ONLY the requested domain" + echo "============================================================" + echo "Front Door: $target_afd" + echo "Resource Group: $target_resource_group" + echo "Domain: $domain_name" + echo "Certificate Type: $certificate_type" + echo "Resource ID: $resource_id" + echo "Provisioning state: $provisioning_state" + echo "Validation state: $state" + echo "============================================================" + echo -if [[ $? -eq 124 ]]; then - echo "ERROR: Timed out after ${SSL_TIMEOUT_SECONDS}s while retrieving TLS certificate." >&2 - exit 1 -fi + ################################################################################ + # Get currently served TLS certificate expiry + ################################################################################ -if [[ -z "$CERT_END_DATE" ]]; then - echo "ERROR: Unable to determine TLS certificate expiry." >&2 - exit 1 -fi + echo "Checking currently served TLS certificate..." -CERT_EXPIRY_SECONDS=$(date -d "$CERT_END_DATE" +%s) -TODAY_SECONDS=$(date -d "$TODAY" +%s) + ssl_timeout_seconds=60 -CERT_DAYS_UNTIL_EXPIRY=$(( (CERT_EXPIRY_SECONDS - TODAY_SECONDS) / 86400 )) + cert_end_date=$( + echo | timeout "${ssl_timeout_seconds}s" openssl s_client \ + -connect "${domain_name}:443" \ + -servername "${domain_name}" \ + 2>/dev/null | + openssl x509 -noout -enddate | + cut -d= -f2 + ) -echo "TLS certificate expires on: $CERT_END_DATE" -echo "TLS certificate expires in approximately: $CERT_DAYS_UNTIL_EXPIRY days" -echo + if [[ $? -eq 124 ]]; then + echo "ERROR: Timed out after ${ssl_timeout_seconds}s while retrieving TLS certificate." >&2 + exit 1 + fi -################################################################################ -# Decide whether validation token regeneration is required -################################################################################ + if [[ -z "$cert_end_date" ]]; then + echo "ERROR: Unable to determine TLS certificate expiry." >&2 + exit 1 + fi -SHOULD_REGENERATE=false + cert_expiry_seconds=$(date -d "$cert_end_date" +%s) + today_seconds=$(date -d "$TODAY" +%s) -CERT_EXPIRATION_THRESHOLD_SECONDS=$(( AFD_DOMAIN_EXPIRATION_DAYS * 86400 )) + cert_days_until_expiry=$(( (cert_expiry_seconds - today_seconds) / 86400 )) -if [[ "$FORCE_REGENERATION" == "true" ]]; then + echo "TLS certificate expires on: $cert_end_date" + echo "TLS certificate expires in approximately: $cert_days_until_expiry days" + echo - SHOULD_REGENERATE=true + ################################################################################ + # Decide whether validation token regeneration is required + ################################################################################ - echo "FORCE regeneration enabled." - echo "Validation token will be regenerated regardless of certificate expiry or validation state." + should_regenerate=false -elif [[ "$STATE" == "PendingRevalidation" || "$STATE" == "InternalError" ]]; then + cert_expiration_threshold_seconds=$(( AFD_DOMAIN_EXPIRATION_DAYS * 86400 )) - SHOULD_REGENERATE=true + if [[ "$FORCE_REGENERATION" == "true" ]]; then - echo "Front Door validation state is $STATE." - echo "Validation token regeneration is required." + should_regenerate=true -elif [[ "$CERT_EXPIRY_SECONDS" -le "$((TODAY_SECONDS + CERT_EXPIRATION_THRESHOLD_SECONDS))" ]]; then + echo "FORCE regeneration enabled." + echo "Validation token will be regenerated regardless of certificate expiry or validation state." - SHOULD_REGENERATE=true + elif [[ "$state" == "PendingRevalidation" || "$state" == "InternalError" ]]; then - echo "TLS certificate expires within $AFD_DOMAIN_EXPIRATION_DAYS days." - echo "Certificate renewal/revalidation is required." + should_regenerate=true -else + echo "Front Door validation state is $state." + echo "Validation token regeneration is required." - SHOULD_REGENERATE=false + elif [[ "$cert_expiry_seconds" -le "$((today_seconds + cert_expiration_threshold_seconds))" ]]; then - echo "TLS certificate has more than $AFD_DOMAIN_EXPIRATION_DAYS days remaining." - echo "No certificate renewal/revalidation required." + should_regenerate=true -fi + echo "TLS certificate expires within $AFD_DOMAIN_EXPIRATION_DAYS days." + echo "Certificate renewal/revalidation is required." -# Force regeneration for testing. This bypasses the normal expiry threshold, -# but deliberately does not submit another request while Front Door is already -# refreshing/submitting a previous regeneration request. -if [[ "$FORCE_REGENERATION" == "true" ]]; then - if [[ "$STATE" == "RefreshingValidationToken" || "$STATE" == "Submitting" ]]; then - SHOULD_REGENERATE=false - echo "Force regeneration requested, but Front Door is already processing a validation token regeneration." - echo "No additional regeneration request will be submitted." else - SHOULD_REGENERATE=true - echo "FORCE regeneration enabled - ignoring validation token expiry threshold." - echo "Attempting asynchronous validation token regeneration." - fi -fi -################################################################################ -# Regenerate token and continue through DNS validation in the same run -################################################################################ - -if [[ "$SHOULD_REGENERATE" == "true" ]]; then + should_regenerate=false - echo - echo "Submitting validation token regeneration..." - echo "Waiting for Front Door to complete regeneration." + echo "TLS certificate has more than $AFD_DOMAIN_EXPIRATION_DAYS days remaining." + echo "No certificate renewal/revalidation required." - if ! REGENERATE_RESULT=$( - az afd custom-domain regenerate-validation-token \ - --ids "$RESOURCE_ID" \ - --only-show-errors \ - 2>&1 - ); then - - echo - echo "ERROR: Failed to regenerate validation token" >&2 - echo "$REGENERATE_RESULT" - echo + fi - if echo "$REGENERATE_RESULT" | grep -Eqi "Rate limit exceeded|Too Many Requests|TooManyRequests|429"; then - echo "WARNING: Azure Front Door rate limit reached." + # Force regeneration for testing. This bypasses the normal expiry threshold, + # but deliberately does not submit another request while Front Door is already + # refreshing/submitting a previous regeneration request. + if [[ "$FORCE_REGENERATION" == "true" ]]; then + if [[ "$state" == "RefreshingValidationToken" || "$state" == "Submitting" ]]; then + should_regenerate=false + echo "Force regeneration requested, but Front Door is already processing a validation token regeneration." + echo "No additional regeneration request will be submitted." + else + should_regenerate=true + echo "FORCE regeneration enabled - ignoring validation token expiry threshold." + echo "Attempting asynchronous validation token regeneration." fi - - exit 1 fi - echo - echo "Validation token regeneration operation completed." - ################################################################################ - # Re-read Front Door until the refreshed token is available + # Regenerate token and continue through DNS validation in the same run ################################################################################ - WAIT_FOR_PENDING_SECONDS=900 - WAIT_INTERVAL_SECONDS=15 - WAITED_SECONDS=0 - - echo "Waiting for Front Door to expose the refreshed validation token..." + if [[ "$should_regenerate" == "true" ]]; then - while true; do + echo + echo "Submitting validation token regeneration..." + echo "Waiting for Front Door to complete regeneration." - DOMAIN=$( - az afd custom-domain show \ - --ids "$RESOURCE_ID" \ - --output json \ - --only-show-errors - ) + if ! regenerate_result=$( + az afd custom-domain regenerate-validation-token \ + --ids "$resource_id" \ + --only-show-errors \ + 2>&1 + ); then - STATE=$(echo "$DOMAIN" | jq -rc '.domainValidationState') - PROVISIONING_STATE=$(echo "$DOMAIN" | jq -rc '.provisioningState') - DOMAIN_TOKEN=$(echo "$DOMAIN" | jq -rc '.validationProperties.validationToken') + echo + echo "ERROR: Failed to regenerate validation token" >&2 + echo "$regenerate_result" + echo - echo "Current validation state: $STATE" - echo "Current provisioning state: $PROVISIONING_STATE" + if echo "$regenerate_result" | grep -Eqi "Rate limit exceeded|Too Many Requests|TooManyRequests|429"; then + echo "WARNING: Azure Front Door rate limit reached." + fi - if [[ "$STATE" == "Pending" && -n "$DOMAIN_TOKEN" && "$DOMAIN_TOKEN" != "null" ]]; then - echo "New validation token is available." - break + exit 1 fi - if [[ "$STATE" == "Approved" && -n "$DOMAIN_TOKEN" && "$DOMAIN_TOKEN" != "null" ]]; then - echo "Front Door is already Approved after regeneration." - break - fi + echo + echo "Validation token regeneration operation completed." - if [[ "$STATE" == "Rejected" || "$STATE" == "InternalError" || "$STATE" == "TimedOut" ]]; then - echo "ERROR: Front Door entered terminal validation state: $STATE" >&2 - exit 1 - fi + ################################################################################ + # Re-read Front Door until the refreshed token is available + ################################################################################ - if [[ "$WAITED_SECONDS" -ge "$WAIT_FOR_PENDING_SECONDS" ]]; then - echo "ERROR: Timed out waiting for the refreshed validation token." >&2 - exit 1 - fi + wait_for_pending_seconds=2700 + wait_interval_seconds=60 + waited_seconds=0 - sleep "$WAIT_INTERVAL_SECONDS" - WAITED_SECONDS=$((WAITED_SECONDS + WAIT_INTERVAL_SECONDS)) + echo "Waiting for Front Door to expose the refreshed validation token..." - done + while true; do - # Refresh values used by the DNS validation section. - DOMAIN_NAME=$(echo "$DOMAIN" | jq -rc '.hostName') - STATE=$(echo "$DOMAIN" | jq -rc '.domainValidationState') - PROVISIONING_STATE=$(echo "$DOMAIN" | jq -rc '.provisioningState') - DOMAIN_VALIDATION_EXPIRY=$(echo "$DOMAIN" | jq -rc '.validationProperties.expirationDate') - DOMAIN_TOKEN=$(echo "$DOMAIN" | jq -rc '.validationProperties.validationToken') - DOMAIN_DNS_ZONE_ID=$(echo "$DOMAIN" | jq -rc '.azureDnsZone.id') + domain=$( + az afd custom-domain show \ + --ids "$resource_id" \ + --output json \ + --only-show-errors + ) - echo "Validation state after regeneration: $STATE" - echo "Retrieved current validation token for DNS validation." - echo + state=$(echo "$domain" | jq -rc '.domainValidationState') + provisioning_state=$(echo "$domain" | jq -rc '.provisioningState') + domain_token=$(echo "$domain" | jq -rc '.validationProperties.validationToken') -fi + echo "Current validation state: $state" + echo "Current provisioning state: $provisioning_state" -################################################################################ -# Process DNS validation for Pending domains -################################################################################ + if [[ "$state" == "Pending" && -n "$domain_token" && "$domain_token" != "null" ]]; then + echo "New validation token is available." + break + fi -if [[ "$STATE" == "Pending" || "$STATE" == "TimedOut" || "$STATE" == "PendingRevalidation" || "$STATE" == "InternalError" ]]; then + if [[ "$state" == "Approved" && -n "$domain_token" && "$domain_token" != "null" ]]; then + echo "Front Door is already Approved after regeneration." + break + fi - echo - echo "Processing DNS validation for $DOMAIN_NAME..." - - ############################################################################## - # Locate DNS zone - ############################################################################## - - DOMAIN_DNS_ZONE=$( - az network dns zone show \ - --ids "$DOMAIN_DNS_ZONE_ID" \ - --output json \ - --only-show-errors | - jq -rc '{ - "name": .name, - "etag": .etag, - "resourceGroup": .resourceGroup - }' - ) + if [[ "$state" == "Rejected" || "$state" == "InternalError" || "$state" == "TimedOut" ]]; then + echo "ERROR: Front Door entered terminal validation state: $state" >&2 + exit 1 + fi - DOMAIN_DNS_ZONE_NAME=$( - echo "$DOMAIN_DNS_ZONE" | - jq -rc '.name' - ) + if [[ "$waited_seconds" -ge "$wait_for_pending_seconds" ]]; then + echo "ERROR: Timed out waiting for the refreshed validation token." >&2 + exit 1 + fi - DOMAIN_DNS_ZONE_RG=$( - echo "$DOMAIN_DNS_ZONE" | - jq -rc '.resourceGroup' - ) + sleep "$wait_interval_seconds" + waited_seconds=$((waited_seconds + wait_interval_seconds)) - echo "DNS zone: $DOMAIN_DNS_ZONE_NAME" - echo "DNS resource group: $DOMAIN_DNS_ZONE_RG" + done - ############################################################################## - # Build DNS TXT record name - ############################################################################## + # Refresh values used by the DNS validation section. + domain_name=$(echo "$domain" | jq -rc '.hostName') + state=$(echo "$domain" | jq -rc '.domainValidationState') + provisioning_state=$(echo "$domain" | jq -rc '.provisioningState') + domain_validation_expiry=$(echo "$domain" | jq -rc '.validationProperties.expirationDate') + domain_token=$(echo "$domain" | jq -rc '.validationProperties.validationToken') + domain_dns_zone_id=$(echo "$domain" | jq -rc '.azureDnsZone.id') - RECORD_SET_NAME_TMP=${DOMAIN_NAME//${DOMAIN_DNS_ZONE_NAME}/} + echo "Validation state after regeneration: $state" + echo "Retrieved current validation token for DNS validation." + echo - RECORD_SET_NAME_TMP="_dnsauth.${RECORD_SET_NAME_TMP}" + fi - RECORD_SET_NAME=${RECORD_SET_NAME_TMP/%./} + ################################################################################ + # Process DNS validation for Pending domains + ################################################################################ - echo "DNS TXT record: $RECORD_SET_NAME" + if [[ "$state" == "Pending" || "$state" == "TimedOut" || "$state" == "PendingRevalidation" || "$state" == "InternalError" ]]; then - ############################################################################## - # Check whether DNS TXT record exists - ############################################################################## + echo + echo "Processing DNS validation for $domain_name..." - echo - echo "Checking DNS Record for validation token..." - - DNS_RESULT=$( - az network dns record-set txt show \ - --zone-name "$DOMAIN_DNS_ZONE_NAME" \ - --name "$RECORD_SET_NAME" \ - --output json \ - --subscription "$AZ_SUBSCRIPTION_SCOPE" \ - --resource-group "$DOMAIN_DNS_ZONE_RG" \ - --only-show-errors \ - 2>&1 - ) + ############################################################################## + # Locate DNS zone + ############################################################################## - DNS_EXIT_CODE=$? + domain_dns_zone=$( + az network dns zone show \ + --ids "$domain_dns_zone_id" \ + --output json \ + --only-show-errors | + jq -rc '{ + "name": .name, + "etag": .etag, + "resourceGroup": .resourceGroup + }' + ) - if [[ "$DNS_EXIT_CODE" -eq 0 ]]; then + domain_dns_zone_name=$( + echo "$domain_dns_zone" | + jq -rc '.name' + ) - RECORD_EXISTS="$DNS_RESULT" + domain_dns_zone_rg=$( + echo "$domain_dns_zone" | + jq -rc '.resourceGroup' + ) - else + echo "DNS zone: $domain_dns_zone_name" + echo "DNS resource group: $domain_dns_zone_rg" - if echo "$DNS_RESULT" | grep -qi "ResourceNotFound\|could not be found\|was not found"; then - RECORD_EXISTS="" - else - echo "ERROR checking DNS TXT record:" >&2 - echo "$DNS_RESULT" + ############################################################################## + # Build DNS TXT record name + ############################################################################## - exit "$DNS_EXIT_CODE" - fi - fi + record_set_name_tmp=${domain_name//${domain_dns_zone_name}/} - ############################################################################## - # Create DNS record if it doesn't exist - ############################################################################## + record_set_name_tmp="_dnsauth.${record_set_name_tmp}" - if [[ -z "$RECORD_EXISTS" ]]; then + record_set_name=${record_set_name_tmp/%./} + + echo "DNS TXT record: $record_set_name" + + ############################################################################## + # Check whether DNS TXT record exists + ############################################################################## echo - echo "DNS TXT Record does not exist." - echo "Creating new record..." - echo "+ New value: $DOMAIN_TOKEN" - - RECORD_SET_STATE=$( - az network dns record-set txt create \ - --zone-name "$DOMAIN_DNS_ZONE_NAME" \ - --name "$RECORD_SET_NAME" \ - --value "$DOMAIN_TOKEN" \ + echo "Checking DNS Record for validation token..." + + dns_result=$( + az network dns record-set txt show \ + --zone-name "$domain_dns_zone_name" \ + --name "$record_set_name" \ --output json \ --subscription "$AZ_SUBSCRIPTION_SCOPE" \ - --resource-group "$DOMAIN_DNS_ZONE_RG" | - jq -rc '.provisioningState' + --resource-group "$domain_dns_zone_rg" \ + --only-show-errors \ + 2>&1 ) - echo - echo "DNS Record creation: $RECORD_SET_STATE" + dns_exit_code=$? - else + if [[ "$dns_exit_code" -eq 0 ]]; then - ############################################################################ - # Record exists - compare tokens - ############################################################################ + record_exists="$dns_result" - RECORD_SET_CURRENT_TOKEN=$( - echo "$RECORD_EXISTS" | - jq -rc '.TXTRecords[0].value[0]' - ) + else - echo - echo "Old DNS token: $RECORD_SET_CURRENT_TOKEN" - echo "New Front Door token: $DOMAIN_TOKEN" + if echo "$dns_result" | grep -qi "ResourceNotFound\|could not be found\|was not found"; then + record_exists="" + else + echo "ERROR checking DNS TXT record:" >&2 + echo "$dns_result" - ############################################################################ - # Update DNS record if token has changed - ############################################################################ + exit "$dns_exit_code" + fi + fi - if [[ "$RECORD_SET_CURRENT_TOKEN" != "$DOMAIN_TOKEN" ]]; then + ############################################################################## + # Create DNS record if it doesn't exist + ############################################################################## + + if [[ -z "$record_exists" ]]; then echo - echo "DNS TXT Record needs updating." - echo "Updating DNS TXT Record..." - - DNS_UPDATE_RESULT=$( - az network dns record-set txt update \ - --zone-name "$DOMAIN_DNS_ZONE_NAME" \ - --name "$RECORD_SET_NAME" \ - --set "txtRecords[0].value[0]=$DOMAIN_TOKEN" \ + echo "DNS TXT Record does not exist." + echo "Creating new record..." + echo "+ New value: $domain_token" + + record_set_state=$( + az network dns record-set txt create \ + --zone-name "$domain_dns_zone_name" \ + --name "$record_set_name" \ + --value "$domain_token" \ --output json \ --subscription "$AZ_SUBSCRIPTION_SCOPE" \ - --resource-group "$DOMAIN_DNS_ZONE_RG" \ - --only-show-errors \ - 2>&1 + --resource-group "$domain_dns_zone_rg" | + jq -rc '.provisioningState' + ) + + echo + echo "DNS Record creation: $record_set_state" + + else + + ############################################################################ + # Record exists - compare tokens + ############################################################################ + + record_set_current_token=$( + echo "$record_exists" | + jq -rc '.TXTRecords[0].value[0]' ) - DNS_EXIT_CODE=$? + echo + echo "Old DNS token: $record_set_current_token" + echo "New Front Door token: $domain_token" + + ############################################################################ + # Update DNS record if token has changed + ############################################################################ - if [[ "$DNS_EXIT_CODE" -ne 0 ]]; then + if [[ "$record_set_current_token" != "$domain_token" ]]; then echo - echo "ERROR: DNS Record update failed" >&2 - echo "$DNS_UPDATE_RESULT" + echo "DNS TXT Record needs updating." + echo "Updating DNS TXT Record..." - exit "$DNS_EXIT_CODE" + dns_update_result=$( + az network dns record-set txt update \ + --zone-name "$domain_dns_zone_name" \ + --name "$record_set_name" \ + --set "txtRecords[0].value[0]=$domain_token" \ + --output json \ + --subscription "$AZ_SUBSCRIPTION_SCOPE" \ + --resource-group "$domain_dns_zone_rg" \ + --only-show-errors \ + 2>&1 + ) - fi + dns_exit_code=$? - RECORD_SET_STATE=$( - echo "$DNS_UPDATE_RESULT" | - jq -r '.provisioningState' - ) + if [[ "$dns_exit_code" -ne 0 ]]; then - echo - echo "DNS Record update: $RECORD_SET_STATE" + echo + echo "ERROR: DNS Record update failed" >&2 + echo "$dns_update_result" - else + exit "$dns_exit_code" - echo - echo "DNS Record already contains the correct validation token." - echo "Nothing to update." + fi + + record_set_state=$( + echo "$dns_update_result" | + jq -r '.provisioningState' + ) + + echo + echo "DNS Record update: $record_set_state" + + else + + echo + echo "DNS Record already contains the correct validation token." + echo "Nothing to update." + + fi fi + else + + echo + echo "Domain validation state is $state." + echo "No DNS update required." + fi -else + ################################################################################ + # Wait for Front Door validation to become Approved + ################################################################################ - echo - echo "Domain validation state is $STATE." - echo "No DNS update required." + if [[ "$state" == "Pending" || "$state" == "PendingRevalidation" ]]; then -fi + wait_for_approved_seconds=2700 + wait_interval_seconds=60 + waited_seconds=0 -################################################################################ -# Wait for Front Door validation to become Approved -################################################################################ + echo + echo "Waiting for Azure Front Door validation state to become Approved..." -if [[ "$STATE" == "Pending" || "$STATE" == "PendingRevalidation" ]]; then + while true; do - WAIT_FOR_APPROVED_SECONDS=900 - WAIT_INTERVAL_SECONDS=15 - WAITED_SECONDS=0 + current_domain=$( + az afd custom-domain show \ + --ids "$resource_id" \ + --output json \ + --only-show-errors + ) - echo - echo "Waiting for Azure Front Door validation state to become Approved..." + current_state=$(echo "$current_domain" | jq -rc '.domainValidationState') + current_provisioning_state=$(echo "$current_domain" | jq -rc '.provisioningState') - while true; do + echo "Current validation state: $current_state" + echo "Current provisioning state: $current_provisioning_state" - CURRENT_DOMAIN=$( - az afd custom-domain show \ - --ids "$RESOURCE_ID" \ - --output json \ - --only-show-errors - ) + if [[ "$current_state" == "Approved" ]]; then + echo + echo "SUCCESS: Azure Front Door validation state is Approved." + break + fi - CURRENT_STATE=$(echo "$CURRENT_DOMAIN" | jq -rc '.domainValidationState') - CURRENT_PROVISIONING_STATE=$(echo "$CURRENT_DOMAIN" | jq -rc '.provisioningState') + if [[ "$current_state" == "Rejected" || "$current_state" == "InternalError" || "$current_state" == "TimedOut" ]]; then + echo + echo "ERROR: Azure Front Door validation entered terminal state: $current_state" >&2 + exit 1 + fi - echo "Current validation state: $CURRENT_STATE" - echo "Current provisioning state: $CURRENT_PROVISIONING_STATE" + if [[ "$waited_seconds" -ge "$wait_for_approved_seconds" ]]; then + echo + echo "ERROR: Front Door did not reach Approved within $wait_for_approved_seconds seconds." >&2 + echo "The DNS record may be correct but Front Door may still be waiting for DNS visibility/TTL expiry." >&2 + exit 1 + fi - if [[ "$CURRENT_STATE" == "Approved" ]]; then - echo - echo "SUCCESS: Azure Front Door validation state is Approved." - break - fi + sleep "$wait_interval_seconds" + waited_seconds=$((waited_seconds + wait_interval_seconds)) - if [[ "$CURRENT_STATE" == "Rejected" || "$CURRENT_STATE" == "InternalError" || "$CURRENT_STATE" == "TimedOut" ]]; then - echo - echo "ERROR: Azure Front Door validation entered terminal state: $CURRENT_STATE" >&2 - exit 1 - fi + done - if [[ "$WAITED_SECONDS" -ge "$WAIT_FOR_APPROVED_SECONDS" ]]; then - echo - echo "ERROR: Front Door did not reach Approved within $WAIT_FOR_APPROVED_SECONDS seconds." >&2 - echo "The DNS record may be correct but Front Door may still be waiting for DNS visibility/TTL expiry." >&2 - exit 1 - fi + fi - sleep "$WAIT_INTERVAL_SECONDS" - WAITED_SECONDS=$((WAITED_SECONDS + WAIT_INTERVAL_SECONDS)) + echo + echo "============================================================" + echo "Finished processing $target_domain" + echo "============================================================" - done +} -fi +################################################################################ +# Process each requested domain +################################################################################ + +FAILED_DOMAINS=() +SUCCEEDED_DOMAINS=() + +for CURRENT_TARGET_DOMAIN in "${TARGET_DOMAINS[@]}"; do + + echo + echo "############################################################" + echo "# Domain: $CURRENT_TARGET_DOMAIN" + echo "############################################################" + echo + # Run in a subshell with errexit enabled so a failure for one domain does not + # stop the remaining domains from being processed. + set +e + ( set -e; process_domain "$CURRENT_TARGET_DOMAIN" ) + DOMAIN_EXIT_CODE=$? + set -e + + if [[ "$DOMAIN_EXIT_CODE" -eq 0 ]]; then + SUCCEEDED_DOMAINS+=("$CURRENT_TARGET_DOMAIN") + else + echo "ERROR: Processing failed for $CURRENT_TARGET_DOMAIN (exit code $DOMAIN_EXIT_CODE)" >&2 + FAILED_DOMAINS+=("$CURRENT_TARGET_DOMAIN") + fi + +done ################################################################################ # Finished @@ -732,5 +815,18 @@ fi echo echo "============================================================" -echo "Finished processing $TARGET_DOMAIN" +echo "Summary" echo "============================================================" +echo "Succeeded: ${#SUCCEEDED_DOMAINS[@]}" +for d in "${SUCCEEDED_DOMAINS[@]+"${SUCCEEDED_DOMAINS[@]}"}"; do + echo " - $d" +done +echo "Failed: ${#FAILED_DOMAINS[@]}" +for d in "${FAILED_DOMAINS[@]+"${FAILED_DOMAINS[@]}"}"; do + echo " - $d" +done +echo "============================================================" + +if [[ "${#FAILED_DOMAINS[@]}" -gt 0 ]]; then + exit 1 +fi