#!/usr/bin/env sh

# gitlab-tofu is a wrapper around the tofu command
# from the OpenTofu project.
#
# It's main purpose is to setup tofu to work inside
# GitLab pipelines and together with the
# OpenTofu CI/CD component.
# Detailed information about it is in the README:
# https://gitlab.com/components/opentofu
#
# Respected Environment Variables:
# --------------------------------
# GITLAB_TOFU_DEBUG: if set to true will enable xtrace.
# GITLAB_TOFU_SOURCE: forces this script in source-mode. Required when source auto-detection fails.
# GITLAB_TOFU_APPLY_NO_PLAN: if set to true, the apply command does not use a plan cache file.
# GITLAB_TOFU_PLAN_NAME: the name of the plan cache and json files. Defaults to `plan`.
# GITLAB_TOFU_PLAN_CACHE: if set is the full path of the plan cache file. Defaults to `<root>/$GITLAB_TOFU_PLAN_NAME.cache`
# GITLAB_TOFU_PLAN_JSON: if set is the full path of the plan json file. Defaults to `<root>/$GITLAB_TOFU_PLAN_NAME.json`
# GITLAB_TOFU_IMPLICIT_INIT: if set to true will perform an implicit `tofu init` before any command that require it. Defaults to `true`.
# GITLAB_TOFU_IGNORE_INIT_ERRORS: if set to true will ignore errors in the `tofu init` command.
# GITLAB_TOFU_INIT_NO_RECONFIGURE: if set to true will not pass `-reconfigure` to the `tofu init` command.
# GITLAB_TOFU_STATE_NAME: the name of the GitLab-managed Terraform state backend endpoint.
#
# Respected OpenTofu Environment Variables:
# > these are variables that are
# > respected if set and avoid using
# > the gitlab-tofu values for them.
# ----------------------------------
# TF_HTTP_USERNAME: username for the HTTP backend. Defaults to `gitlab-ci-token`.
# TF_HTTP_PASSWORD: password for the HTTP backend. Defaults to `$CI_JOB_TOKEN`.
# TF_CLI_CONFIG_FILE: will not overwrite if set. Defaults to `$HOME/.terraformrc` if it exists.
#
# Respected GitLab CI/CD Variables:
# > these are variables exposed by
# > GitLab CI/CD and respected by
# > the gitlab-tofu script for
# > certain configurations.
# CI_JOB_TOKEN:
#   - used as default value for TF_HTTP_PASSWORD.
#   - used as value for TF_TOKEN_<host> variable.
# CI_PROJECT_DIR:
#   - used as default value for root directory.

# set some shell options
set -o errexit

if [ "${GITLAB_TOFU_DEBUG}" = "true" ]; then
    set -o xtrace
fi

# Feature Flags
# =============
# Below are a bunch of variables that we use as "feature flags".

# There are no feature flags at the moment.

# Helpers

# Evaluate if this script is being sourced or executed directly.
# See https://stackoverflow.com/a/28776166
sourced=0
# shellcheck disable=SC2153 # it's actually a different variable, thanks shelllcheck.
if [ "$GITLAB_TOFU_SOURCE" = 'true' ]; then
  sourced=1
elif [ -n "$ZSH_VERSION" ]; then
  case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
elif [ -n "$KSH_VERSION" ]; then
  # shellcheck disable=SC2296
  [ "$(cd -- "$(dirname -- "$0")" && pwd -P)/$(basename -- "$0")" != "$(cd -- "$(dirname -- "${.sh.file}")" && pwd -P)/$(basename -- "${.sh.file}")" ] && sourced=1
elif [ -n "$BASH_VERSION" ]; then
  (return 0 2>/dev/null) && sourced=1
else # All other shells: examine $0 for known shell binary filenames.
     # Detects `sh` and `dash`; add additional shell filenames as needed.
  case ${0##*/} in sh|-sh|dash|-dash) sourced=1;; esac
fi

# Dependencies
# ============
# Defines all the external dependencies and checks if they exist, if not, abort with an error.
dependencies="dirname basename pwd sed idn2 jq tofu"

if [ -n "$ZSH_VERSION" ]; then
  # ZSH is the only supported SHELL that does not split by word by default,
  # so we set this option to actually do it.
  setopt sh_word_split
fi

for dep in $dependencies; do
  if ! command -v "$dep" >/dev/null 2>&1; then
    echo "Error: gitlab-tofu is missing dependency: '$dep'" >&2
    exit 1
  fi
done

if [ -n "$ZSH_VERSION" ]; then
  # see comment above when setting sh_word_split.
  unsetopt sh_word_split
fi

# Deprecations
# ============
if [ -n "$TF_STATE_NAME" ]; then
  echo 'WARNING: you have manually set the deprecated TF_STATE_NAME environment variable. Please use the GITLAB_TOFU_STATE_NAME environment variable instead. The TF_STATE_NAME variable will be removed soon.' >&2

  if [ -n "$GITLAB_TOFU_STATE_NAME" ]; then
    echo 'WARNING: you have set GITLAB_TOFU_STATE_NAME environment variable in addition to the deprecated TF_STATE_NAME. This causes a conflict and GITLAB_TOFU_STATE_NAME will be used exclusively' >&2
  else
    GITLAB_TOFU_STATE_NAME="$TF_STATE_NAME"
  fi
fi

JQ_PLAN='
  (
    [.resource_changes[]?.change.actions?] | flatten
  ) | {
    "create":(map(select(.=="create")) | length),
    "update":(map(select(.=="update")) | length),
    "delete":(map(select(.=="delete")) | length)
  }
'

# Default state backend credentials to gitlab-ci-token/CI_JOB_TOKEN
state_backend_username="gitlab-ci-token"
state_backend_password="${CI_JOB_TOKEN}"

# If TF_ADDRESS is unset but GITLAB_TOFU_STATE_NAME is provided, then default to GitLab backend in current project
if [ -n "${GITLAB_TOFU_STATE_NAME}" ] && [ -z "${TF_ADDRESS}" ]; then
  # auto url-encode GITLAB_TOFU_STATE_NAME
  GITLAB_TOFU_STATE_NAME="$(jq -rn --arg x "${GITLAB_TOFU_STATE_NAME}" '$x|@uri')"
  TF_ADDRESS="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${GITLAB_TOFU_STATE_NAME}"
fi

if [ -z "${GITLAB_TOFU_PLAN_NAME}" ]; then
  GITLAB_TOFU_PLAN_NAME=plan
fi

if [ -z "${GITLAB_TOFU_APPLY_NO_PLAN}" ]; then
  GITLAB_TOFU_APPLY_NO_PLAN=false
fi

# If TF_ROOT is set then use the -chdir option
if [ -n "${TF_ROOT}" ]; then
  abs_tf_root=$(cd "${CI_PROJECT_DIR}"; realpath "${TF_ROOT}")

  tf_chdir_opt="-chdir=${abs_tf_root}"

  default_tf_plan_cache="${abs_tf_root}/${GITLAB_TOFU_PLAN_NAME}.cache"
  default_tf_plan_json="${abs_tf_root}/${GITLAB_TOFU_PLAN_NAME}.json"
fi


# If GITLAB_TOFU_PLAN_CACHE is not set then use either the plan.cache file within TF_ROOT if set, or plan.cache in CWD
if [ -z "${GITLAB_TOFU_PLAN_CACHE}" ]; then
  GITLAB_TOFU_PLAN_CACHE="${default_tf_plan_cache:-${GITLAB_TOFU_PLAN_NAME}.cache}"
fi

# If GITLAB_TOFU_PLAN_JSON is not set then use either the plan.json file within TF_ROOT if set, or plan.json in CWD
if [ -z "${GITLAB_TOFU_PLAN_JSON}" ]; then
  GITLAB_TOFU_PLAN_JSON="${default_tf_plan_json:-${GITLAB_TOFU_PLAN_NAME}.json}"
fi

# Set variables for the HTTP backend to default to TF_* values
export TF_HTTP_ADDRESS="${TF_HTTP_ADDRESS:-${TF_ADDRESS}}"
export TF_HTTP_LOCK_ADDRESS="${TF_HTTP_LOCK_ADDRESS:-${TF_ADDRESS}/lock}"
export TF_HTTP_LOCK_METHOD="${TF_HTTP_LOCK_METHOD:-POST}"
export TF_HTTP_UNLOCK_ADDRESS="${TF_HTTP_UNLOCK_ADDRESS:-${TF_ADDRESS}/lock}"
export TF_HTTP_UNLOCK_METHOD="${TF_HTTP_UNLOCK_METHOD:-DELETE}"
export TF_HTTP_USERNAME="${TF_HTTP_USERNAME:-${state_backend_username}}"
export TF_HTTP_PASSWORD="${TF_HTTP_PASSWORD:-${state_backend_password}}"
export TF_HTTP_RETRY_WAIT_MIN="${TF_HTTP_RETRY_WAIT_MIN:-5}"

# Expose Gitlab specific variables to terraform since no -tf-var is available
# The following variables are deprecated because they do not conform to
# HCL naming best practices. Use the lower snake_case variants below instead.
export TF_VAR_CI_JOB_ID="${TF_VAR_CI_JOB_ID:-${CI_JOB_ID}}"
export TF_VAR_CI_COMMIT_SHA="${TF_VAR_CI_COMMIT_SHA:-${CI_COMMIT_SHA}}"
export TF_VAR_CI_JOB_STAGE="${TF_VAR_CI_JOB_STAGE:-${CI_JOB_STAGE}}"
export TF_VAR_CI_PROJECT_ID="${TF_VAR_CI_PROJECT_ID:-${CI_PROJECT_ID}}"
export TF_VAR_CI_PROJECT_NAME="${TF_VAR_CI_PROJECT_NAME:-${CI_PROJECT_NAME}}"
export TF_VAR_CI_PROJECT_NAMESPACE="${TF_VAR_CI_PROJECT_NAMESPACE:-${CI_PROJECT_NAMESPACE}}"
export TF_VAR_CI_PROJECT_PATH="${TF_VAR_CI_PROJECT_PATH:-${CI_PROJECT_PATH}}"
export TF_VAR_CI_PROJECT_URL="${TF_VAR_CI_PROJECT_URL:-${CI_PROJECT_URL}}"


export TF_VAR_ci_job_id="${TF_VAR_ci_job_id:-${CI_JOB_ID}}"
export TF_VAR_ci_commit_sha="${TF_VAR_ci_commit_sha:-${CI_COMMIT_SHA}}"
export TF_VAR_ci_job_stage="${TF_VAR_ci_job_stage:-${CI_JOB_STAGE}}"
export TF_VAR_ci_project_id="${TF_VAR_ci_project_id:-${CI_PROJECT_ID}}"
export TF_VAR_ci_project_name="${TF_VAR_ci_project_name:-${CI_PROJECT_NAME}}"
export TF_VAR_ci_project_namespace="${TF_VAR_ci_project_namespace:-${CI_PROJECT_NAMESPACE}}"
export TF_VAR_ci_project_path="${TF_VAR_ci_project_path:-${CI_PROJECT_PATH}}"
export TF_VAR_ci_project_url="${TF_VAR_ci_project_url:-${CI_PROJECT_URL}}"

# Use terraform automation mode (will remove some verbose unneeded messages)
export TF_IN_AUTOMATION=true

# Set a Terraform CLI Configuration File
default_tf_cli_config_file="$HOME/.terraformrc"
if [ -z "${TF_CLI_CONFIG_FILE}" ] && [ -f "${default_tf_cli_config_file}" ]; then
  export TF_CLI_CONFIG_FILE="${default_tf_cli_config_file}"
fi


terraform_authenticate_private_registry() {
  # From Terraform 1.2.0 and later (or all versions of OpenTofu), we can use TF_TOKEN_your_domain_name to authenticate to registry.
  # The credential environment variable has the following requirements:
  # - Domain names containing non-ASCII characters are converted to their punycode equivalent with an ACE prefix
  # - Periods are encoded as underscores
  # - Hyphens are encoded as double underscores
  # For more info, see https://www.terraform.io/cli/config/config-file#environment-variable-credentials
  if [ "${CI_SERVER_PROTOCOL}" = "https" ] && [ -n "${CI_SERVER_HOST}" ]; then
    tf_token_var_name=TF_TOKEN_$(idn2 "${CI_SERVER_HOST}" | sed 's/\./_/g' | sed 's/-/__/g')
    # If TF_TOKEN_ for the Gitlab domain is not set then use the CI_JOB_TOKEN
    if [ -z "$(eval "echo \${${tf_token_var_name}:-}")" ]; then
      export "${tf_token_var_name}"="${CI_JOB_TOKEN}"
    fi
  fi
}

# If GITLAB_TOFU_IMPLICIT_INIT is not set, we set it to `true`.
# If set to `true` it will call `terraform init` prior
# to calling the wrapper `terraform` commands.
GITLAB_TOFU_IMPLICIT_INIT=${GITLAB_TOFU_IMPLICIT_INIT:-true}

# Allows users to continue the actual command in case init failed
GITLAB_TOFU_IGNORE_INIT_ERRORS=${GITLAB_TOFU_IGNORE_INIT_ERRORS:-false}

terraform_init() {
  # If GITLAB_TOFU_INIT_NO_RECONFIGURE is not set to 'true',
  # a `-reconfigure` flag is added to the `terraform init` command.
  if [ "$GITLAB_TOFU_INIT_NO_RECONFIGURE" != 'true' ]; then
    tf_init_reconfigure_flag='-reconfigure'
  fi

  # We want to allow word splitting here for TF_INIT_FLAGS
  # shellcheck disable=SC2086
  tofu "${tf_chdir_opt}" init "${@}" -input=false ${tf_init_reconfigure_flag} ${TF_INIT_FLAGS} \
    1>&2 || $GITLAB_TOFU_IGNORE_INIT_ERRORS
}

# If this script is executed and not sourced, a terraform command is ran.
# Otherwise, nothing happens and the sourced shell can use the defined variables
# and helper functions exposed by this script.
if [ $sourced -eq 0 ]; then
  # Authenticate to private registry
  terraform_authenticate_private_registry

  var_file_args=""
  if [ -n "${GITLAB_TOFU_VAR_FILE}" ]; then
    var_file_args="--var-file=${GITLAB_TOFU_VAR_FILE}"
  fi

  case "${1}" in
    "apply")
      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init
      if [ "$GITLAB_TOFU_APPLY_NO_PLAN" = false ]; then
        tofu "${tf_chdir_opt}" "${@}" -input=false -auto-approve "${GITLAB_TOFU_PLAN_CACHE}"
      else
        # shellcheck disable=SC2086
        tofu "${tf_chdir_opt}" "${@}" -input=false -auto-approve ${var_file_args}
      fi
    ;;
    "destroy")
      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init
      tofu "${tf_chdir_opt}" "${@}" -auto-approve
    ;;
    "fmt")
      tofu "${tf_chdir_opt}" "${@}" -check -diff -recursive
    ;;
    "init")
      # shift argument list „one to the left“ to not call 'terraform init init'
      shift
      terraform_init "${@}"
    ;;
    "plan")
      plan_args=''
      if [ "${GITLAB_TOFU_USE_DETAILED_EXITCODE}" = 'true' ]; then
        plan_args='-detailed-exitcode'
      fi

      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init
      # shellcheck disable=SC2086
      tofu "${tf_chdir_opt}" "${@}" -input=false -out="${GITLAB_TOFU_PLAN_CACHE}" ${var_file_args} ${plan_args} && ret=$? || ret=$?

      if [ "${GITLAB_TOFU_PLAN_WITH_JSON}" = 'true' ]; then
        if [ "$ret" -eq 0 ] || [ "$ret" -eq 2 ]; then
          if ! tofu "${tf_chdir_opt}" show -json "${GITLAB_TOFU_PLAN_CACHE}" | jq -r "${JQ_PLAN}" > "${GITLAB_TOFU_PLAN_JSON}"; then
            exit $?
          fi

          # NOTE: we want to exit with the tofu plan exit code if the tofu show command call is successful.
          exit "$ret"
        fi
      fi

      exit "$ret"
    ;;
    "plan-json")
      tofu "${tf_chdir_opt}" show -json "${GITLAB_TOFU_PLAN_CACHE}" | jq -r "${JQ_PLAN}" > "${GITLAB_TOFU_PLAN_JSON}"
    ;;
    "validate")
      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init -backend=false
      # shellcheck disable=SC2086
      tofu "${tf_chdir_opt}" "${@}" ${var_file_args}
    ;;
    "test")
      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init -backend=false
      # shellcheck disable=SC2086
      tofu "${tf_chdir_opt}" "${@}" ${var_file_args}
    ;;
    "graph")
      $GITLAB_TOFU_IMPLICIT_INIT && terraform_init
      # shellcheck disable=SC2086
      tofu "${tf_chdir_opt}" "${@}" ${var_file_args}
    ;;
    --)
      shift
      tofu "${tf_chdir_opt}" "${@}"
    ;;
    *)
      tofu "${tf_chdir_opt}" "${@}"
    ;;
  esac
else
  # This variable can be used if the script is sourced
  # shellcheck disable=SC2034
  GITLAB_TOFU_SOURCED=true
fi
