Skip to content

Instantly share code, notes, and snippets.

@rquadling
Created April 17, 2025 13:09
Show Gist options
  • Save rquadling/693bf50d98311c601fe9f80881478141 to your computer and use it in GitHub Desktop.
Save rquadling/693bf50d98311c601fe9f80881478141 to your computer and use it in GitHub Desktop.
Generate terraform state manipulations for the Terraform Cloudflare Provider upgrade from v4 to v5.
#!/usr/bin/env bash
# Check if arguments are provided (either tfvars files or default to terraform.tfvars)
TFVARS_FILES=("$@") # Collect arguments into an array
# If no arguments are provided, default to terraform.tfvars
if [ ${#TFVARS_FILES[@]} -eq 0 ]; then
TFVARS_FILES=("terraform.tfvars")
fi
# Check if the BACKEND_KEY_NAME environment variable is set
if [ -z "$BACKEND_KEY_NAME" ]; then
echo "Error: BACKEND_KEY_NAME environment variable is not set."
exit 1
fi
# Initialize a variable to track the backend config
BACKEND_CONFIG="-backend-config key=${BACKEND_KEY_NAME}.tfstate"
# Inform the user which tfvars files are being used
echo "Using the following tfvars files: ${TFVARS_FILES[@]}"
# Ensure we're in the right repository directory (this assumes you've manually cd'd into the correct repo)
if [ ! -d ".git" ]; then
echo "Error: This script must be run from within a Git repository. Please ensure you're in the correct repo directory."
exit 1
fi
# Inform the user which backend config is being used
echo "Using backend config: $BACKEND_CONFIG"
# Initialize Terraform for this repository (must be run to configure the remote backend)
echo "Running terraform init..."
terraform init -input=false $BACKEND_CONFIG -upgrade "${TFVARS_FILES[@]/#/--var-file=}"
# Fetch the current remote state and store it in a temporary file
STATE_FILE=$(mktemp)
terraform state pull > "$STATE_FILE"
# Run terraform plan with the provided tfvars files
echo "Running terraform plan..."
terraform plan "${TFVARS_FILES[@]/#/--var-file=}"
# Generate state removal and import commands for Cloudflare DNS records
echo "Generating state rm and import commands for Cloudflare DNS records..."
echo ''
echo '' > ../migrations.sh
chmod +x ../migrations.sh
# Define a search / replace map
declare -A RESOURCE_TYPE_MAP=(
["cloudflare_page_rule"]="cloudflare_page_rule"
["cloudflare_record"]="cloudflare_dns_record"
)
# Build a comma-separated list of quoted keys for the jq IN(...) call
JQ_TYPE_FILTER=$(printf '.type == "%s" or ' "${!RESOURCE_TYPE_MAP[@]}" | sed 's/ or $//')
# List the resources in the remote state
# Use jq to filter for cloudflare_record resources, preserving module and resource name
jq -crM '.resources[] | select('"$JQ_TYPE_FILTER"') | {module: .module, name: .name, type: .type, instances: .instances[]}' "$STATE_FILE" | while read -r line; do
# Extract module, resource name, zone_id, and RESOURCE_ID
MODULE_NAME=$(echo "$line" | jq -crM '.module')
RESOURCE_NAME=$(echo "$line" | jq -crM '.name')
RESOURCE_TYPE=$(echo "$line" | jq -crM '.type')
INDEX_KEY=$(echo "$line" | jq -crM '.instances.index_key')
ZONE_ID=$(echo "$line" | jq -crM '.instances.attributes.zone_id')
RESOURCE_ID=$(echo "$line" | jq -crM '.instances.attributes.id')
# If RESOURCE_ID are missing, skip this entry
if [[ -z "$RESOURCE_ID" ]]; then
echo "Error: Missing RESOURCE_ID for $line"
continue
fi
# Wrap index_key if it is present
if [[ -n "$INDEX_KEY" && "$INDEX_KEY" != "null" ]]; then
if [[ "$INDEX_KEY" =~ ^[0-9]+$ ]]; then
# Numeric index, no quotes
INDEX_KEY="[$INDEX_KEY]"
else
# String index, quoted
INDEX_KEY='["'"$INDEX_KEY"'"]'
fi
else
INDEX_KEY=''
fi
# Get the new type from the map
NEW_RESOURCE_TYPE="${RESOURCE_TYPE_MAP[$RESOURCE_TYPE]}"
if [[ -z "$NEW_RESOURCE_TYPE" ]]; then
echo "Error: No mapping found for resource type: $RESOURCE_TYPE"
continue
fi
# Determine if the resource is in a module or root module
if [ "$MODULE_NAME" == "root" ] || [ -z "$MODULE_NAME" ] || [ "$MODULE_NAME" == "null" ]; then
OLD_RESOURCE_PATH="$RESOURCE_TYPE.$RESOURCE_NAME"
NEW_RESOURCE_PATH="$NEW_RESOURCE_TYPE.$RESOURCE_NAME"
else
OLD_RESOURCE_PATH="$MODULE_NAME.$RESOURCE_TYPE.$RESOURCE_NAME"
NEW_RESOURCE_PATH="$MODULE_NAME.$NEW_RESOURCE_TYPE.$RESOURCE_NAME"
fi
# Generate the terraform state rm command with single quotes
echo "terraform state rm '$OLD_RESOURCE_PATH$INDEX_KEY'" >> ../migrations.sh
# Generate the terraform import command with correct zone_id/RESOURCE_ID format
if [[ -n "$ZONE_ID" && "$ZONE_ID" != "null" ]]; then
echo "terraform import "${TFVARS_FILES[@]/#/--var-file=}" '$NEW_RESOURCE_PATH$INDEX_KEY' '$ZONE_ID/$RESOURCE_ID'" >> ../migrations.sh
else
echo "terraform import "${TFVARS_FILES[@]/#/--var-file=}" '$NEW_RESOURCE_PATH$INDEX_KEY' '$RESOURCE_ID'" >> ../migrations.sh
fi
done
# Clean up the temporary state file
rm "$STATE_FILE"
echo ''
cat ../migrations.sh
echo ''
printf "Run \e[92m../migrations.sh\e[0m to run the above"
echo ''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment