diff --git a/README.md b/README.md index bc0f9a4..523c7fe 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,11 @@ Inspired by https://stackoverflow.com/a/45968410 # Installing -## Terraform -Install terraform, make sure ``terraform-0.11`` is on the path - ## Main script Copy ``tf.sh somewhere``, ``/usr/local/bin/tf`` for instance. +Terraform in installed based on the `terraform.version` file, via tfswitch. + ## Bash prompt Copy ``tf-prompt.sh`` somewhere, source it from your bash profile. @@ -81,21 +80,25 @@ Execute the script from a directory inside a root (i.e. one of the parent direct Select a stack: - tf stack network + tf stack select network Select a workspace: - tf workspace acc - + tf workspace select acc + +* creating workspace requires write permissions because the buckets are encrypted with KMS keys + Login with PUM: - tf login + Run pum-aws retrieve the credentials Do some stuff: tf plan tf apply +Use `tf help` to see supported tf (custom and terraform) commands. + ``global.tf``, ``global.tfvars`` and ``envvars/.tfvars`` are automatically used when running: * ``apply`` * ``destroy`` @@ -120,9 +123,12 @@ How to create the backend: ## Subsequent modifications Switching between workspaces: -* run `tf.sh workspace ` +* run `tf.sh workspace select ` * run `tf.sh backend init` +## Check current context (stack/workspace) + +* tf ctx # Dependency graph @@ -140,7 +146,7 @@ See examples: [dependency graph 1](deps-status-1.png), [dependency graph 2](deps The wrapper expects an executable named ``terraform-0.11`` to be on the path. You can use version 0.12 or 0.14 in this way: * create a file named ``terraform.version`` on the root of your repo, or in a stack to use that version for a single stack -* file should contain ``0.12`` or ``0.14`` to select the version +* file should contain ``0.12.0`` or ``0.14.0`` to select the version # Extra credentials @@ -162,4 +168,10 @@ This file should be excluded from source control. It will be loaded during ``pla 1. Bash autocompletion 1. Initialize a project. 1. Use symlinks if supported instead of copying ``global.tf`` as ``global.symlink.tf`` -1. ? Find current stack from current directory, to be able to use ``cd stacks/xxx`` instead of ``tf stack xxx`` +1. ? Find current stack from current directory, to be able to use ``cd stacks/xxx`` instead of ``tf stack select xxx`` + +# Limitations + +* the workspace is tied with the AWS account. Can't have multiple workspaces under the same AWS account +* terraform.version file is required in the root directory or at the stack level +** the contents of the version needs to exactly match (e.g. 1.5.7) diff --git a/tf.sh b/tf.sh index 963abe3..83772e7 100755 --- a/tf.sh +++ b/tf.sh @@ -7,11 +7,7 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color -function usage { - echo "Usage: $0 " - exit 1; -} - +## utility contains() { if [[ $1 =~ (^|[[:space:]])$2($|[[:space:]]) ]] then @@ -21,97 +17,85 @@ contains() { fi } -# Login with Privileged User Management -# 1st parameter: target AWS profile -# 2nd parameter: target AWS account id -function login() { - set +e - EXIT_CODE=$(winpty --version 2>&1 > /dev/null) - EXIT_CODE=$? - set -e - PTY= - if [[ $EXIT_CODE == 0 ]] - then - PTY="winpty" - fi +context_stack() { + if [ -n "$TERRAFORM_STACK" ]; then + return 0 + fi - PYTHON=python - set +e - EXIT_CODE=$(python3 --version 2>&1 > /dev/null) - EXIT_CODE=$? - set -e - if [[ $EXIT_CODE == 0 ]] - then - PYTHON="python3" - fi + if [ ! -e $STACK_FILE ]; then + echo -e "${RED}Stack file doesn't exist!${NC}" + return 1 + fi - $PTY $PYTHON $TFDIR/pum-aws.py --profile $1 --account $2 -} + export TERRAFORM_STACK=$(cat $STACK_FILE) -function setup_workspace { - cd $DIR/stacks/$TERRAFORM_STACK - CURRENT_WORKSPACE=`$TERRAFORM_BIN workspace show` - if [ $CURRENT_WORKSPACE != $TERRAFORM_WORKSPACE ] - then - init_workspace + if [ -z "$TERRAFORM_STACK" ]; then + echo -e "${RED}Missing value from stack file!${NC}" + return 1 fi + + return 0 } -function backend { - if [ -z $TF_USE_CURRENT_PROFILE ] - then +context_workspace() { + if [ -n "$TERRAFORM_WORKSPACE" ]; then export AWS_PROFILE=$TERRAFORM_WORKSPACE + return 0 fi - echo "Using directory 'state-management'" - cd $DIR/state-management - cp $DIR/backend.tf backend.symlink.tf + if [ ! -e $WORKSPACE_FILE ]; then + echo -e "${RED}Workspace file doesn't exist!${NC}" + return 1 + fi - BACKEND_BUCKET="terraform-state-${accounts[${TERRAFORM_WORKSPACE}]}" - STATE_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/terraform-state\`].{keyid:TargetKeyId}" --output text) - if [ "$1" == "init" ]; then - rm -rf .terraform terraform.tfstate.d .terraform.lock.hcl - $TERRAFORM_BIN $1 ${@:2} -backend-config="bucket=${BACKEND_BUCKET}" -backend-config="key=backend/terraform.tfstate" -backend-config="encrypt=true" -backend-config="kms_key_id=${STATE_KEY_ID}" - else - $TERRAFORM_BIN $@ + export TERRAFORM_WORKSPACE=$(cat $WORKSPACE_FILE) + export AWS_PROFILE=$TERRAFORM_WORKSPACE + if [ -z "$TERRAFORM_WORKSPACE" ]; then + echo -e "${RED}Missing value from workspace file!${NC}" + return 1 fi + + return 0 } -function init_workspace { - if [ -z $TF_USE_CURRENT_PROFILE ] - then - export AWS_PROFILE=$TERRAFORM_WORKSPACE +context() { + context_stack + if [ $? -eq 1 ]; then + exit 1 fi - cd $DIR/stacks/$TERRAFORM_STACK - - rm -rf .terraform terraform.tfstate.d .terraform.lock.hcl - BACKEND_BUCKET="terraform-state-${accounts[${TERRAFORM_WORKSPACE}]}" - STATE_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/terraform-state\`].{keyid:TargetKeyId}" --output text) - if [ $TERRAFORM_BIN = "terraform-0.14" ]; then - EXTRA=-upgrade + + context_workspace + if [ $? -eq 1 ]; then + exit 1 fi - $TERRAFORM_BIN init -backend-config="bucket=${BACKEND_BUCKET}" -backend-config="key=stacks/$TERRAFORM_STACK" -backend-config="encrypt=true" -backend-config="kms_key_id=${STATE_KEY_ID}" $EXTRA - set +e - $TERRAFORM_BIN workspace select $TERRAFORM_WORKSPACE - if [ $? != 0 ] + + if [ $1 ] || [ "$TF_WRAPPER_DEBUG" == "true" ] then - $TERRAFORM_BIN workspace new $TERRAFORM_WORKSPACE - $TERRAFORM_BIN workspace select $TERRAFORM_WORKSPACE + echo -e "Using stack: ${GREEN}${TERRAFORM_STACK}${NC}" + echo -e "Using workspace: ${GREEN}${TERRAFORM_WORKSPACE}${NC}" fi - set -e } -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink - TFDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - SOURCE="$(readlink "$SOURCE")" - [[ $SOURCE != /* ]] && SOURCE="$TFDIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located -done -TFDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" +install_required() { + if [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "${YELLOW}Running install_required${NC}" + fi -DIR= + if [ "$(which tfswitch)" == "" ]; then + echo "tfswitch is not installed...\nInstalling latest version of tfswitch" + curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/release/install.sh -o /tmp/install.sh && chmod 755 /tmp/install.sh + /tmp/install.sh -b $HOME/bin && rm /tmp/install.sh + echo "${GREEN}Tfswitch has been installed${NC}" + elif [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "${GREEN}tfswitch is installed${NC}" + fi +} search_up() { + if [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "${YELLOW}Running search_up${NC}" + fi + local LOOK=${PWD%/} while [[ -n $LOOK ]]; do [[ -d $LOOK/.tf ]] && { @@ -126,338 +110,480 @@ search_up() { fi } -search_up +terraform_version_select() { + if [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "${YELLOW}Running terraform_version_select${NC}" + fi -#SOURCE="${BASH_SOURCE[0]}" -#while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink -# DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" -# SOURCE="$(readlink "$SOURCE")" -# [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located -#done -#DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" + # Define which terraform version to use + context_stack + TF_VERSION_FILE=$DIR/stacks/$TERRAFORM_STACK/terraform.version + TF_VERSION_FILE_ROOT=$DIR/terraform.version -if [ "$1" = "init" ] -then - if [ "x$DIR" = "x" ] - then - mkdir -p .tf - search_up - echo -e "${GREEN}Initialized directory $DIR${NC}" - exit 0 + #TF_VERSION_FILE needs to match the SEMVAR MAJOR.MINOR.PATCH + if [ -f "$TF_VERSION_FILE" ]; then + TF_VERSION=`head -n 1 $TF_VERSION_FILE` + elif [ -f "$TF_VERSION_FILE_ROOT" ]; then + TF_VERSION=`head -n 1 $TF_VERSION_FILE_ROOT` else - (>&2 echo -e "${RED}Already initialized in $DIR${NC}") + (>&2 echo -e "${RED}No version of terraform has been specified. Create first a terraform.version file with a proper semver${NC}") exit 1 fi -fi - -if [ "x$DIR" = "x" ] -then - (>&2 echo -e "${RED}No initialized directory.${NC}") - exit 1 -else - echo -e "${GREEN}Root: $DIR${NC}" -fi - -WORKSPACE_FILE=$DIR/.tf/.workspace -STACK_FILE=$DIR/.tf/.stack -ACCOUNTS_FILE=$DIR/accounts - -## Read accounts -if ! [[ -e $ACCOUNTS_FILE ]] -then - (>&2 echo -e "${RED}$ACCOUNTS_FILE file missing.${NC}") - exit 1 -fi - -declare -A accounts -while IFS=$'=' read -r -a anArray -do - accounts[${anArray[0]}]=`echo ${anArray[1]} | sed s/[^0-9]//` -done < $ACCOUNTS_FILE - -if ! [ -d $DIR/stacks ] -then - (>&2 echo -e "${RED}No current stack. Create 'stacks' directory and use: $0 stack [stack name].${NC}") - exit 1 -fi - -VALID_STACKS=`find $DIR/stacks -maxdepth 1 -mindepth 1 -type d -printf "%f " | tr ' ' '\n' | sort | tr '\n' ' '` - -## Select stack -if [ "x$TERRAFORM_STACK" = "x" ] && [ "$1" = "stack" ] -then - shift - if contains "$VALID_STACKS" $1 - then - echo -e "${YELLOW}Switching stack to $1.${NC}" - export TERRAFORM_STACK=$1 - echo $TERRAFORM_STACK > $STACK_FILE - echo -e "${GREEN}Stack switched to $TERRAFORM_STACK.${NC}" - exit 0 - else - (>&2 echo -e "${RED}stack '$1' is invalid. Usage: $0 stack [$VALID_STACKS].${NC}") + + if ! [[ $TF_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "The version specified in the 'terraform.version', doesn't match semver MAJOR.MINOR.PATCH" exit 1 fi -fi -## Verify stack -if [ "x$TERRAFORM_STACK" != "x" ] -then - if contains "$VALID_STACKS" $TERRAFORM_STACK - then - echo -e "${GREEN}Current stack: $TERRAFORM_STACK (from env)${NC}" - else - (>&2 echo -e "${RED}Invalid stack '$TERRAFORM_STACK' (from env). Valid stacks: $VALID_STACKS.${NC}") - exit 1 + if [ "$(terraform version | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" != "$TF_VERSION" ]; then + echo -e "${YELLOW}Terraform couldn't be found or it didn't match the $TF_VERSION${NC} version" + tfswitch "$TF_VERSION" fi -elif [[ -e $STACK_FILE ]] -then - STACK=`cat $STACK_FILE` - if contains "$VALID_STACKS" $STACK - then - export TERRAFORM_STACK=$STACK - echo -e "${GREEN}Current stack: $TERRAFORM_STACK${NC}" - else - (>&2 echo -e "${RED}No current stack. Use: $0 stack [stack name].${NC}") - fi -else - (>&2 echo -e "${RED}No current stack. Usage: $0 stack [$VALID_STACKS].${NC}") - exit 1 -fi - -# Define which terraform version to use -TF_VERSION_FILE=$DIR/stacks/$TERRAFORM_STACK/terraform.version -TF_VERSION_FILE_ROOT=$DIR/terraform.version -TERRAFORM_BIN="terraform-0.11" - -if [ -f "$TF_VERSION_FILE" ]; then - TF_VERSION=`cat $TF_VERSION_FILE` -elif [ -f "$TF_VERSION_FILE_ROOT" ]; then - TF_VERSION=`cat $TF_VERSION_FILE_ROOT` -fi - -if [ -n "$TF_VERSION" ]; then - if [ $TF_VERSION = "0.14" ]; then - echo "Working with terraform v0.14 ..." - TERRAFORM_BIN="terraform-0.14" - elif [ $TF_VERSION = "0.12" ]; then - echo "Working with terraform v0.12 ..." - TERRAFORM_BIN="terraform-0.12" - elif [ $TF_VERSION = "0.13" ]; then - echo "Working with terraform v0.13 ..." - TERRAFORM_BIN="terraform-0.13" - elif [ $TF_VERSION = "0.15" ]; then - echo "Working with terraform v0.15 ..." - TERRAFORM_BIN="terraform-0.15" - elif [ $TF_VERSION = "1.0" ]; then - echo "Working with terraform v1.0 ..." - TERRAFORM_BIN="terraform-1.0" - else - echo "The used version from terraform.version file is wrong or missing!" +} + +read_accounts() { + if [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "${YELLOW}Running read_accounts${NC}" + fi + + ACCOUNTS_FILE=$DIR/accounts + if ! [[ -e $ACCOUNTS_FILE ]]; then + (>&2 echo -e "${RED}$ACCOUNTS_FILE file missing.${NC}") exit 1 fi -fi -# The stack exists, switch to it -cd $DIR/stacks/$TERRAFORM_STACK -cp $DIR/global.tf global.symlink.tf -cp $DIR/backend.tf backend.symlink.tf + while IFS=$'=' read -r -a anArray + do + accounts[${anArray[0]}]=`echo ${anArray[1]} | sed s/[^0-9]//` + done < $ACCOUNTS_FILE +} -if [ "$1" = "login" ] -then - if [ "x$TERRAFORM_WORKSPACE" = "x" ] +bootstrap() { + if [ "$TF_WRAPPER_DEBUG" == "true" ]; then + echo -e "Running in ${YELLOW}debug${NC} mode" + echo -e "${YELLOW}Running bootstrap${NC}" + fi + + install_required + search_up + + if [ ! -d "$DIR/.tf" ]; then + mkdir -p $DIR/.tf + fi + + read_accounts + + # Setup variables used throughout the script + WORKSPACE_FILE=$DIR/.tf/.workspace + STACK_FILE=$DIR/.tf/.stack + VALID_STACKS=$(find $DIR/stacks -maxdepth 1 -mindepth 1 -type d -printf "%f " | tr ' ' '\n' | sort | tr '\n' ' ') + + # Local configuration file, for some api keys + if [ -f "$DIR/terraform.tfvars" ]; then + EXTRA_VAR_FILE=-var-file=../../terraform.tfvars + fi + + terraform_version_select +} + +## workspace + +workspace() { + WORKSPACE_SUBCOMMAND=$1 + shift + case $WORKSPACE_SUBCOMMAND in + select) + workspace_select "$@" + ;; + list) + context_workspace + echo -e "Currently using workspace: [${GREEN}$TERRAFORM_WORKSPACE${NC}]" + echo -e "Available workspaces are: [${YELLOW}${!accounts[@]}${NC}]" + ;; + help) + echo -e "Usage: ${YELLOW}$TF_BIN_NAME workspace ${NC} +Available subcommands: + +select Selects the working workspace +list List the selected workspace and available workspaces +help Prints available workspace subcommands + +${YELLOW}To initialize an already selected workspace use '$TF_BIN_NAME init'${NC}" + ;; + *) + echo -e "The command ${RED}$WORKSPACE_SUBCOMMAND${NC} is not available.\nUse '${YELLOW}$TF_BIN_NAME workspace help${NC}' to see the available command" + ;; + esac + return $? +} + +workspace_init() { + if (! stack_verify) then + echo -e "${RED}An error occured while verifying the stack${NC}" + return 1 + fi + + if [ -z $TF_USE_CURRENT_PROFILE ] then - TERRAFORM_WORKSPACE=`cat $WORKSPACE_FILE` + export AWS_PROFILE=$TERRAFORM_WORKSPACE fi - if [ ${accounts[$TERRAFORM_WORKSPACE]+abc} ] + STACK_DIR=$DIR/stacks/$TERRAFORM_STACK + + rm -rf $STACK_DIR/.terraform $STACK_DIR/terraform.tfstate.d $STACK_DIR/.terraform.lock.hcl + BACKEND_BUCKET="terraform-state-${accounts[${TERRAFORM_WORKSPACE}]}" + STATE_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/terraform-state\`].{keyid:TargetKeyId}" --output text) + + terraform -chdir=$STACK_DIR init -backend-config="bucket=${BACKEND_BUCKET}" -backend-config="key=stacks/$TERRAFORM_STACK" -backend-config="encrypt=true" -backend-config="kms_key_id=${STATE_KEY_ID}" + set +e + terraform -chdir=$STACK_DIR workspace select $TERRAFORM_WORKSPACE + if [ $? != 0 ] then - echo -e "${GREEN}Current workspace: $TERRAFORM_WORKSPACE${NC}" + terraform -chdir=$STACK_DIR workspace new $TERRAFORM_WORKSPACE + terraform -chdir=$STACK_DIR workspace select $TERRAFORM_WORKSPACE + fi + set -e + return $? +} + +workspace_verify() { + workspace_valid() { + if [ ${accounts[$TERRAFORM_WORKSPACE]+abc} ]; then + echo -e "Current workspace: ${GREEN}$TERRAFORM_WORKSPACE${NC}" + workspace_init else - (>&2 echo -e "${RED}No current workspace. Usage: $0 workspace [${!accounts[@]}].${NC}") - exit 1 + (>&2 echo -e "${RED}Invalid workspace '$TERRAFORM_WORKSPACE'.\n$TF_BIN_NAME workspace select \nValid workspaces: [${!accounts[@]}].${NC}") + return 1 + fi + } + + # Keeping this if for automation purposes via environment variables + if [ -n "$TERRAFORM_WORKSPACE" ]; then + if (! workspace_valid "env") then + return 1 + fi + else + context_workspace + if (! workspace_valid ".tf/.workspace") then + return 1 + fi fi - login $TERRAFORM_WORKSPACE ${accounts[$TERRAFORM_WORKSPACE]} - exit 0; -fi + return 0 +} -## Select (and setup) workspace -if [ "x$TERRAFORM_WORKSPACE" = "x" ] && [ "$1" = "workspace" ] -then - shift - if [ "x$1" != "x" ] && [ ${accounts[$1]+abc} ] +workspace_select() { + if [ -n "$1" ] && [ ${accounts[$1]+abc} ] then - export TERRAFORM_WORKSPACE=$1 - echo $TERRAFORM_WORKSPACE > $WORKSPACE_FILE - echo -e "${YELLOW}Switching workspace to $TERRAFORM_WORKSPACE${NC}" - setup_workspace - echo -e "${GREEN}Workspace switched to $TERRAFORM_WORKSPACE${NC}" - exit 0 + context_workspace + if [ "$1" != "$TERRAFORM_WORKSPACE" ]; then + export TERRAFORM_WORKSPACE=$1 + echo $TERRAFORM_WORKSPACE > $WORKSPACE_FILE + echo -e "${YELLOW}Switching workspace to $TERRAFORM_WORKSPACE${NC}" + workspace_verify + else + echo -e "Already on workspace ${GREEN}$TERRAFORM_WORKSPACE${NC}" + fi else - (>&2 echo -e "${RED}workspace '$1' is invalid. Usage: $0 workspace [${!accounts[@]}].${NC}") - exit 1 + (>&2 echo -e "${RED}workspace '$1' is invalid. Usage: $TF_BIN_NAME workspace accounts[@]}].${NC}") + return 1 fi -fi + return 0 +} -## Verify workspace -if [ "x$TERRAFORM_WORKSPACE" != "x" ] -then - if [ ${accounts[$TERRAFORM_WORKSPACE]+abc} ] - then - echo -e "${GREEN}Current workspace: $TERRAFORM_WORKSPACE (from env)${NC}" - setup_workspace +## stack + +stack() { + STACK_SUBCOMMAND=$1 + shift + case $STACK_SUBCOMMAND in + select) + stack_select "$@" + ;; + list) + context_stack + echo -e "Currently using stack: [${GREEN}$TERRAFORM_STACK${NC}]" + echo -e "Available stacks are: [${YELLOW}$VALID_STACKS${NC}]" + ;; + help) + echo -e "Usage: ${YELLOW}$TF_BIN_NAME stack ${NC} +Available subcommands: + +select Selects the working stack +list List the selected stack and available stacks +help Prints available stack subcommands" + ;; + *) + echo -e "The command ${RED}$STACK_SUBCOMMAND${NC} is not available.\nUse '${YELLOW}$TF_BIN_NAME stack help${NC}' to see the available command" + ;; + esac + return $? +} + +stack_verify() { + stack_valid(){ + if contains "$VALID_STACKS" $TERRAFORM_STACK + then + echo -e "Current stack: ${GREEN}$TERRAFORM_STACK${NC}" + else + (>&2 echo -e "${RED}Invalid stack '$TERRAFORM_STACK'.\n$TF_BIN_NAME stack select [$VALID_STACKS]${NC}") + return 1 + fi + return 0 + } + + # Keeping this if for automation purposes via environment variables + if [ -n "$TERRAFORM_STACK" ]; then + if (! stack_valid "env") then + return 1 + fi else - (>&2 echo -e "${RED}Invalid workspace '$TERRAFORM_WORKSPACE'. Valid workspaces: [${!accounts[@]}].${NC}") - exit 1 + context_stack + if (! stack_valid ".tf/.stack") then + return 1 + fi fi -elif [[ -e $WORKSPACE_FILE ]] -then - WORKSPACE=`cat $WORKSPACE_FILE` - if [ ${accounts[$WORKSPACE]+abc} ] + + + stack_global=$DIR/stacks/$TERRAFORM_STACK/global.symlink.tf + stack_backend=$DIR/stacks/$TERRAFORM_STACK/backend.symlink.tf + if [ -f $stack_global ]; then + rm $stack_global + fi + + if [ ! -h $stack_global ]; then + ln -s $DIR/global.tf $stack_global + fi + + if [ -f $stack_backend ]; then + rm $stack_backend + fi + + if [ ! -h $stack_backend ]; then + ln -s $DIR/backend.tf $stack_backend + fi + return $? +} + +stack_select() { + if contains "$VALID_STACKS" $1 then - export TERRAFORM_WORKSPACE=$WORKSPACE - echo -e "${GREEN}Current workspace: $TERRAFORM_WORKSPACE${NC}" - setup_workspace + echo -e "${YELLOW}Switching stack to $1.${NC}" + export TERRAFORM_STACK=$1 + echo $TERRAFORM_STACK > $STACK_FILE + echo -e "${GREEN}Stack switched to $TERRAFORM_STACK.${NC}" else - (>&2 echo -e "${RED}No current workspace. Usage: $0 workspace [${!accounts[@]}].${NC}") - exit 1 + (>&2 echo -e "${RED}stack '$1' is invalid. Usage: $TF_BIN_NAME stack [$VALID_STACKS]${NC}") + return 1 fi -else - (>&2 echo -e "${RED}No current workspace. Usage: $0 workspace [${!accounts[@]}].${NC}") - exit 1 -fi -if [ -z $TF_USE_CURRENT_PROFILE ] -then - export AWS_PROFILE=$TERRAFORM_WORKSPACE -fi + terraform_version_select + return $? +} + +## backend +backend() { + if [ ${#@} -lt 1 ]; then + echo -e "${RED}No command has been provided for backend.\nUsual terraform commands can be used.${NC}" + echo -e "${YELLOW}Usage: $TF_BIN_NAME backend ${NC}" + return 1 + fi + context_workspace + + echo -e "Using directory: ${GREEN}state-management${NC}" + echo -e "Current workspace: ${GREEN}$TERRAFORM_WORKSPACE${NC}" + backend=$DIR/state-management/backend.symlink.tf + if [ -f $backend ]; then + rm $backend + fi -# Local configuration file, for some api keys -if [ -f "$DIR/terraform.tfvars" ]; then - EXTRA_VAR_FILE=-var-file=../../terraform.tfvars -fi + if [ ! -h $backend ]; then + ln -s $DIR/backend.tf $backend + fi -TF_COMMAND=$1 -shift + BACKEND_BUCKET="terraform-state-${accounts[${TERRAFORM_WORKSPACE}]}" + STATE_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/terraform-state\`].{keyid:TargetKeyId}" --output text) + if [ "$1" == "init" ]; then + rm -rf .terraform terraform.tfstate.d .terraform.lock.hcl + terraform -chdir=$DIR/state-management $1 ${@:2} -backend-config="bucket=${BACKEND_BUCKET}" -backend-config="key=backend/terraform.tfstate" -backend-config="encrypt=true" -backend-config="kms_key_id=${STATE_KEY_ID}" + else + terraform -chdir=$DIR/state-management "$@" + fi + return $? +} -case $TF_COMMAND in - deps) - ADD_STATUS=d - if [ "x$1" = "xstatus" ] +## dependencies +dependencies() { + ADD_STATUS=d + if [ "x$1" = "xstatus" ] + then + ADD_STATUS=1 + fi + # check dependencies between modules + echo -e "digraph { + compound = \"true\" + newrank = \"true\" + node[style=filled]\n" + for stack in $VALID_STACKS + do + ATTRIBUTES= + if [ "$ADD_STATUS" = "1" ] then - ADD_STATUS=1 + COLOR=red + set +e + result=$(export TERRAFORM_STACK=$stack; $0 plan -detailed-exitcode 2> /dev/null) + EXIT_CODE=$? + set -e + if [[ $EXIT_CODE == 2 ]] + then + COLOR=yellow + elif [[ $EXIT_CODE == 0 ]] + then + COLOR=green + fi + if [[ -n "$ATTRIBUTES" ]] + then + ATTRIBUTES=${ATTRIBUTES}, + fi + ATTRIBUTES=${ATTRIBUTES}fillcolor=${COLOR} fi - # check dependencies between modules - echo -e "digraph { - compound = \"true\" - newrank = \"true\" - node[style=filled] -" - for stack in $VALID_STACKS - do - ATTRIBUTES= - if [ "$ADD_STATUS" = "1" ] + echo -e \"$stack\"[$ATTRIBUTES] + done + grep \\\"terraform_remote_state $DIR/stacks/*/*.tf | sed -e 's/.*\/stacks\///; s/\/.*terraform_remote_state//; s/["{]//g; s/ /" -> "/; s/\s\+$/"/; s/^/"/' + + echo -e "}" +} + +## chamber +chamber() { + export AWS_REGION=eu-west-1 + COMMAND=$1 + case $COMMAND in + exec|help|history|list) + chamber "$@" + return $? + ;; + read|write) + shift + SERVICE=$1 + shift + export CHAMBER_KMS_KEY_ALIAS=alias/$SERVICE-configuration-secrets + chamber $COMMAND $SERVICE "$@" + return $? + ;; + esac + return 1 +} + +## conf +conf() { + PREFIX=$1 + shift + COMMAND=$1 + case $COMMAND in + diff) + shift + FILE=$1 + shift + if [ -z $FILE ] then - COLOR=red - set +e - result=$(export TERRAFORM_STACK=$stack; $0 plan -detailed-exitcode 2> /dev/null) - EXIT_CODE=$? - set -e - if [[ $EXIT_CODE == 2 ]] - then - COLOR=yellow - elif [[ $EXIT_CODE == 0 ]] - then - COLOR=green - fi - if [[ -n "$ATTRIBUTES" ]] - then - ATTRIBUTES=${ATTRIBUTES}, - fi - ATTRIBUTES=${ATTRIBUTES}fillcolor=${COLOR} + (>&2 echo -e "${RED}Specify a file name${NC}") + exit 1 fi - echo -e \"$stack\"[$ATTRIBUTES] - done - grep \\\"terraform_remote_state $DIR/stacks/*/*.tf | sed -e 's/.*\/stacks\///; s/\/.*terraform_remote_state//; s/["{]//g; s/ /" -> "/; s/\s\+$/"/; s/^/"/' - - echo -e "}" - exit 0; - ;; - conf) - PREFIX=$1 - shift - COMMAND=$1 - case $COMMAND in - diff) - shift - FILE=$1 - shift - if [ -z $FILE ] - then - (>&2 echo -e "${RED}Specify a file name${NC}") - exit 1 - fi - aws s3 cp s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/$FILE - | diff $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/$FILE "$@" -- - - ;; - cp) - shift - CONF_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/$PREFIX-configuration\`].{keyid:TargetKeyId}" --output text) - aws s3 cp --sse "aws:kms" --sse-kms-key-id "${CONF_KEY_ID}" $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ --recursive "$@" - ;; - sync) - shift - CONF_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/$PREFIX-configuration\`].{keyid:TargetKeyId}" --output text) - aws s3 sync --sse "aws:kms" --sse-kms-key-id "${CONF_KEY_ID}" --delete $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ - ;; - "") - echo "Files on S3:" - aws s3 ls --recursive s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ - echo "Sync (dryrun):" - aws s3 sync --delete --dryrun $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ - ;; - esac - exit $? - ;; - chamber) - export AWS_REGION=eu-west-1 - COMMAND=$1 - case $COMMAND in - exec|help|history|list) - chamber "$@" - exit $? - ;; - read|write) - shift - SERVICE=$1 - shift - export CHAMBER_KMS_KEY_ALIAS=alias/$SERVICE-configuration-secrets - chamber $COMMAND $SERVICE "$@" - exit $? - ;; - esac - exit 1; - ;; - apply) - export TF_CLI_ARGS="-var-file=../../envvars/${TERRAFORM_WORKSPACE}.tfvars -var-file=../../global.tfvars $EXTRA_VAR_FILE -input=false -auto-approve=false" - ;; - plan|validate|destroy|import|refresh) - export TF_CLI_ARGS="-var-file=../../envvars/${TERRAFORM_WORKSPACE}.tfvars -var-file=../../global.tfvars $EXTRA_VAR_FILE -input=false" - ;; - get|help|state|graph|fmt|show|taint|untaint|version|output) - # Should work without any change - ;; - re-init) - init_workspace - exit $? - ;; - backend) - backend $@ - exit $? - ;; - *) - (>&2 echo -e "${RED}Command $TF_COMMAND is unsupported! Use terraform $TF_COMMAND at your own risks...${NC}") - exit 1; - ;; -esac - -$TERRAFORM_BIN $TF_COMMAND "$@" -exit $? + aws s3 cp s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/$FILE - | diff $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/$FILE "$@" -- - + ;; + cp) + shift + CONF_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/$PREFIX-configuration\`].{keyid:TargetKeyId}" --output text) + aws s3 cp --sse "aws:kms" --sse-kms-key-id "${CONF_KEY_ID}" $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ --recursive "$@" + ;; + sync) + shift + CONF_KEY_ID=$(aws kms list-aliases --query "Aliases[?AliasName==\`alias/$PREFIX-configuration\`].{keyid:TargetKeyId}" --output text) + aws s3 sync --sse "aws:kms" --sse-kms-key-id "${CONF_KEY_ID}" --delete $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ + ;; + *) + echo "Files on S3:" + aws s3 ls --recursive s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ + echo "Sync (dryrun):" + aws s3 sync --delete --dryrun $DIR/configurationfiles/$PREFIX/$TERRAFORM_WORKSPACE/ s3://$PREFIX-configuration-${accounts[$TERRAFORM_WORKSPACE]}/ + ;; + esac + return $? +} + +## main +main() { + bootstrap + + TF_COMMAND=$1 + shift + case $TF_COMMAND in + deps) + context + dependencies "$@" + ;; + conf) + context + conf "$@" + ;; + chamber) + chamber "$@" + ;; + init) + workspace_verify + ;; + backend) + backend "$@" + ;; + apply) + context + export TF_CLI_ARGS="-var-file=$DIR/envvars/${TERRAFORM_WORKSPACE}.tfvars -var-file=$DIR/global.tfvars $EXTRA_VAR_FILE -input=false -auto-approve=false" + terraform -chdir=$DIR/stacks/$TERRAFORM_STACK $TF_COMMAND "$@" + ;; + plan|destroy|import|refresh|console) + context + export TF_CLI_ARGS="-var-file=$DIR/envvars/${TERRAFORM_WORKSPACE}.tfvars -var-file=$DIR/global.tfvars $EXTRA_VAR_FILE -input=false" + terraform -chdir=$DIR/stacks/$TERRAFORM_STACK $TF_COMMAND "$@" + ;; + get|validate|state|graph|fmt|show|taint|untaint|version|output|force-unlock|metadata|login|logout|providers) + context + terraform -chdir=$DIR/stacks/$TERRAFORM_STACK $TF_COMMAND "$@" + ;; + workspace) + workspace "$@" + ;; + stack) + stack "$@" + ;; + ctx) + context true + ;; + help) + echo -e "${YELLOW}Available $TF_BIN_NAME commands\n$TF_BIN_NAME has priority over terraform${NC}" + echo " + deps Displays dependencies between stacks + conf Interact with S3 configuration buckets + chamber Run chamber commands + init Forcefully initialize a workspace + ctx Show selected stack and workspace + workspace Wraps workspaces around 'accounts' file + backend Run normal terraform commands on the terraform state bucket + stack Manage stacks for the selected workspace" + echo -e "\n${YELLOW}Available terraform commands${NC}\n" + terraform -help + ;; + *) + (>&2 echo -e "${RED}Command '$TF_COMMAND' is not supported. Use '$TF_BIN_NAME help' for more information${NC}") + echo -e "${YELLOW}$TF_BIN_NAME is compatible with terrafom version ~>1.5. If you require a command that is above this version, please raise an issue at https://github.com/vismaosscomponents/terraform-wrapper${NC}" + ;; + esac + + exit $? +} + +## Running script +# Get binary name for output purposes +TF_BIN_NAME=$(echo $0 | rev | cut -d '/' -f1 | rev) +# globally scoped +declare -A accounts +main "$@" +## End of running script