Skip to content

Instantly share code, notes, and snippets.

@mbrodala
Created March 3, 2025 08:05
Show Gist options
  • Save mbrodala/62f0de7807fc7a5b5fb9dc8052bd5e6a to your computer and use it in GitHub Desktop.
Save mbrodala/62f0de7807fc7a5b5fb9dc8052bd5e6a to your computer and use it in GitHub Desktop.
CloudFlare: import zones + DNS records
#!/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