Last active
August 8, 2022 08:35
-
-
Save hoo29/9a69b88a76dc66549df7d3e791252788 to your computer and use it in GitHub Desktop.
Bash script to migrate Terraform workspcae state from Hashicorp Terraform Cloud to Scalr.
This file contains 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 | |
# This script was used to migrate Terraform workspace state from Hashicorp Terraform Cloud (TFC) to Scalr. | |
# It loops through all projects in a given directory, extracts the workspace id, downloads the state from TFC, and then uploads it to Scalr. | |
# It was made for the Ably Terraform projects directory structure and so will unlikely work as-is. It can easily be tweaked to support other folder structures. | |
# The workspaces in Scalr need to already exist. | |
set -e | |
set -o pipefail | |
# Call with ws_id as arg | |
function move_state() { | |
echo "$1" | |
echo "downloading state" | |
TFC_WS_ID=$(curl -sfS \ | |
--header "Authorization: Bearer $TFC_TOKEN" \ | |
--header "Content-Type: application/vnd.api+json" \ | |
"https://app.terraform.io/api/v2/organizations/ablyrealtime/workspaces/${1}" | jq '.data.id' -r) | |
echo "tfc ws id $TFC_WS_ID" | |
SCALR_WS_ID=$(curl -sfS \ | |
--header 'Prefer: profile=preview' \ | |
--header 'Content-Type: application/vnd.api+json' \ | |
--header "Authorization: Bearer $SCALR_TOKEN" \ | |
"https://ablyrealtime.scalr.io/api/iacp/v3/workspaces?filter%5Bname%5D=${1}" | jq '.data[0].id' -r) | |
echo "scalr ws id $SCALR_WS_ID" | |
if [ "$SCALR_WS_ID" == "null" ]; then | |
echo "SCALR_WS_ID came back null" | |
exit 1 | |
fi | |
STATE_DOWNLOAD_URL=$(curl -sfS \ | |
--header "Authorization: Bearer $TFC_TOKEN" \ | |
--header "Content-Type: application/vnd.api+json" \ | |
"https://app.terraform.io/api/v2/workspaces/${TFC_WS_ID}/current-state-version" | jq '.data.attributes["hosted-state-download-url"]' -r) | |
OUTPUT_PATH="${1}.json" | |
curl -sfS "$STATE_DOWNLOAD_URL" -o "$OUTPUT_PATH" | |
SERIAL=$(cat "$OUTPUT_PATH" | jq '.serial' -r) | |
MD5=$(md5sum "$OUTPUT_PATH" | cut -d ' ' -f 1) | |
LINEAGE=$(cat "$OUTPUT_PATH" | jq '.lineage' -r) | |
B64_STATE=$(cat "$OUTPUT_PATH" | base64 -w 0) | |
echo "check existing scalr state" | |
EXISTING_SCALR_STATE=$(curl -sS \ | |
--header 'Prefer: profile=preview' \ | |
--header 'Content-Type: application/vnd.api+json' \ | |
--header "Authorization: Bearer $SCALR_TOKEN" \ | |
"https://ablyrealtime.scalr.io/api/iacp/v3/workspaces/${SCALR_WS_ID}/current-state-version") | |
SCALR_LINEAGE=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.lineage' -r) | |
SCALR_SERIAL=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.serial' -r) | |
SCALR_MD5=$(echo "$EXISTING_SCALR_STATE" | jq '.data.attributes.md5' -r) | |
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SCALR_LINEAGE" != "$LINEAGE" ]; then | |
echo "scalr state has different lineage for ${1}, skipping" | |
return | |
fi | |
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SERIAL" -eq "$SCALR_SERIAL" ]; then | |
echo "scalr state has the same serial for ${1}, skipping" | |
if [ "$SCALR_MD5" != "$MD5" ]; then | |
echo "WARNING state serials were the same but checksum is different for ${1}, what is different?" | |
fi | |
return | |
fi | |
if [ "$SCALR_LINEAGE" != "null" ] && [ "$SERIAL" -lt "$SCALR_SERIAL" ]; then | |
echo "scalr state has larger serial for ${1}, skipping" | |
return | |
fi | |
echo "uploading new state" | |
# Save to file because some states are big | |
# You can add force if needed \"force\" : \"true\", \ | |
echo -n "{ \ | |
\"data\": { \ | |
\"type\": \"state-versions\", \ | |
\"attributes\" : { \ | |
\"serial\" : \"${SERIAL}\", \ | |
\"md5\" : \"${MD5}\", \ | |
\"lineage\" : \"${LINEAGE}\", \ | |
\"state\" : \"${B64_STATE}\" \ | |
} \ | |
} \ | |
}" >curl_payload.json | |
curl --fail-with-body \ | |
--header "Authorization: Bearer $SCALR_TOKEN" \ | |
--header "Content-Type: application/vnd.api+json" \ | |
--request POST \ | |
--data @curl_payload.json \ | |
"https://ablyrealtime.scalr.io/api/tfe/v2/workspaces/${SCALR_WS_ID}/state-versions" | |
} | |
function main() { | |
# Top level folder containing your terraform projects (assumes monorepo setup) | |
BASE_DIR=REPLACE_ME | |
if [ -z "$TFC_TOKEN" ]; then | |
echo "TFC_TOKEN not set" | |
exit 1 | |
fi | |
if [ -z "$SCALR_TOKEN" ]; then | |
echo "SCALR_TOKEN not set" | |
exit 1 | |
fi | |
# Loops through folders in format BASE_DIR/TERRAFORM_PROJECT/ENVIRONMENT | |
for SUB_DIR in "BASE_DIR"/*/; do | |
if [ -d "$SUB_DIR" ]; then | |
for TENV in "$SUB_DIR"*/; do | |
if [ -d "$TENV" ] && test -f "${TENV}main.tf"; then | |
echo "$TENV" | |
WS_NAME=$(tr '\n' ' ' <"${TENV}main.tf" | sed -ne 's#^.*workspaces\s*{\s*name\s*=\s*"\([^"]*\)".*$#\1#p') | |
if [ -z "$WS_NAME" ]; then | |
echo "WS_NAME was null for $TENV, skipping" | |
else | |
echo "moving state" | |
move_state "$WS_NAME" | |
fi | |
fi | |
done | |
fi | |
done | |
echo "done" | |
} | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment