Created
March 3, 2025 08:05
-
-
Save mbrodala/62f0de7807fc7a5b5fb9dc8052bd5e6a to your computer and use it in GitHub Desktop.
CloudFlare: import zones + DNS records
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -euo pipefail | |
# Load environment variables from .env file | |
. .env | |
# Required Configuration | |
CF_API_TOKEN="${CF_API_TOKEN:-}" | |
CF_ACCOUNT_ID="${CF_ACCOUNT_ID:-}" | |
ZONE_FILES_DIR="${ZONE_FILES_DIR:-}" | |
# Fail early if required variables are not set | |
if [[ -z "$CF_API_TOKEN" || -z "$CF_ACCOUNT_ID" || -z "$ZONE_FILES_DIR" ]]; then | |
echo "Error: Missing required environment variables." | |
echo "Ensure .env contains:" | |
echo " CF_API_TOKEN=<your_api_token>" | |
echo " CF_ACCOUNT_ID=<your_account_id>" | |
echo " ZONE_FILES_DIR=<your_zone_files_directory>" | |
exit 1 | |
fi | |
CLOUDFLARE_API_BASE="https://api.cloudflare.com/client/v4" | |
# Rate limit handling | |
RATE_LIMIT_WAIT=2 # Normal wait time between requests | |
RATE_LIMIT_RETRY=3600 # Wait 1 hour (3600 seconds) if rate limit is hit | |
MAX_RETRIES=3 # Maximum retries per request | |
# Function to perform an API request with rate limit handling and success check | |
perform_api_request() { | |
local method="$1" | |
local endpoint="$2" | |
local data="${3:-}" | |
local is_file_upload="${4:-false}" | |
local url="${CLOUDFLARE_API_BASE}${endpoint}" | |
local headers=( | |
--header "Authorization: Bearer $CF_API_TOKEN" | |
) | |
if [[ "$is_file_upload" == "true" ]]; then | |
headers+=(--header "Content-Type: multipart/form-data") | |
else | |
headers+=(--header "Content-Type: application/json") | |
fi | |
local attempt=0 | |
local response | |
local status_code | |
local success | |
while (( attempt < MAX_RETRIES )); do | |
sleep "$RATE_LIMIT_WAIT" | |
if [[ "$is_file_upload" == "true" ]]; then | |
response=$(curl --silent --show-error --write-out "\n%{http_code}" --request "$method" "$url" "${headers[@]}" --form "$data") | |
elif [[ -n "$data" ]]; then | |
response=$(curl --silent --show-error --write-out "\n%{http_code}" --request "$method" "$url" "${headers[@]}" --data "$data") | |
else | |
response=$(curl --silent --show-error --write-out "\n%{http_code}" --request "$method" "$url" "${headers[@]}") | |
fi | |
status_code=$(echo "$response" | tail -n1) | |
response=$(echo "$response" | head -n1) | |
success=$(echo "$response" | jq --raw-output '.success') | |
if [[ "$status_code" -eq 200 || "$status_code" -eq 201 ]] && [[ "$success" == "true" ]]; then | |
echo "$response" | |
return 0 | |
elif [[ "$status_code" -eq 429 ]]; then | |
echo "Rate limit exceeded. Waiting for an hour before retrying..." >&2 | |
sleep "$RATE_LIMIT_RETRY" | |
else | |
echo "API request failed (HTTP $status_code): $(echo "$response" | jq --raw-output '.errors[0].message')" >&2 | |
exit 1 | |
fi | |
((attempt++)) | |
done | |
echo "Max retries reached. Exiting." >&2 | |
exit 1 | |
} | |
# Function to get the Zone ID for a given domain | |
get_zone_id() { | |
local domain="$1" | |
local endpoint="/zones?account.id=${CF_ACCOUNT_ID}&name=$domain" | |
response=$(perform_api_request "GET" "$endpoint") | |
echo "$response" | jq --raw-output '.result[0].id' | |
} | |
# Function to delete a Cloudflare zone | |
delete_zone() { | |
local domain="$1" | |
local zone_id | |
zone_id=$(get_zone_id "$domain") | |
if [[ -n "$zone_id" && "$zone_id" != "null" ]]; then | |
local endpoint="/zones/$zone_id" | |
response=$(perform_api_request "DELETE" "$endpoint") | |
success=$(echo "$response" | jq --raw-output '.success') | |
if [[ "$success" == "true" ]]; then | |
echo "Zone $domain deleted successfully." | |
else | |
echo "Error deleting zone $domain." >&2 | |
exit 1 | |
fi | |
else | |
echo "Zone $domain does not exist or could not be retrieved. Skipping deletion." | |
fi | |
} | |
# Function to create a Cloudflare zone | |
create_zone() { | |
local domain="$1" | |
local endpoint="/zones" | |
local data=$(jq --compact-output --null-input --argjson account $(jq -c -n --arg id ${CF_ACCOUNT_ID} '$ARGS.named') --arg name ${domain} --arg type full '$ARGS.named') | |
response=$(perform_api_request "POST" "$endpoint" "$data") | |
echo "$response" | jq --raw-output '.result.id' | |
} | |
# Function to import DNS records from a zone file using bulk import | |
import_dns_records() { | |
local zone_id="$1" | |
local domain="$2" | |
local zone_file="$ZONE_FILES_DIR/$domain" | |
if [[ ! -f "$zone_file" ]]; then | |
echo "No zone file found for $domain. Skipping..." >&2 | |
return | |
fi | |
local endpoint="/zones/$zone_id/dns_records/import?proxied=false" | |
local data="file=@$zone_file" | |
response=$(perform_api_request "POST" "$endpoint" "$data" "true") | |
success=$(echo "$response" | jq --raw-output '.success') | |
if [[ "$success" == "true" ]]; then | |
echo "DNS records for $domain imported successfully." | |
else | |
echo "Error importing DNS records for $domain." >&2 | |
exit 1 | |
fi | |
} | |
# Main script logic | |
if [[ ! -d "$ZONE_FILES_DIR" ]]; then | |
echo "Error: Directory $ZONE_FILES_DIR does not exist." | |
exit 1 | |
fi | |
for zone_file in $(ls -1 "$ZONE_FILES_DIR"/* | sort --random-sort); do | |
domain=$(basename "$zone_file") | |
echo "Deleting zone: $domain..." | |
delete_zone "$domain" | |
echo "Creating zone: $domain..." | |
zone_id=$(create_zone "$domain") | |
echo "Importing DNS records for $domain to zone $zone_id using bulk import with grey cloud (DNS only)..." | |
import_dns_records "$zone_id" "$domain" | |
done | |
echo "All zones processed." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment