diff --git a/src/gitlab-tofu.sh b/src/gitlab-tofu.sh index 672c02d34208c5ec8268774bb23ed2ca9b530194..e5ecfc1505eaddb0480a84eaeeb77f61d24dae45 100644 --- a/src/gitlab-tofu.sh +++ b/src/gitlab-tofu.sh @@ -107,7 +107,7 @@ 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" +dependencies="dirname basename pwd sed idn2 jq tofu curl" if [ -n "$ZSH_VERSION" ]; then # ZSH is the only supported SHELL that does not split by word by default, @@ -428,6 +428,9 @@ if [ $sourced -eq 0 ]; then # shellcheck disable=SC2086 tofu "${tf_chdir_opt}" "${@}" ${var_file_args} ;; + "delete-state") + curl --request DELETE -u "${backend_username}:${backend_password}" "${backend_address}" + ;; --) shift tofu "${tf_chdir_opt}" "${@}" diff --git a/templates/destroy.yml b/templates/destroy.yml index 413b2eda01120e397890ca3913f5767f6aee54e7..90b1e55d43a753681b82c43d8df0636a6621bee5 100644 --- a/templates/destroy.yml +++ b/templates/destroy.yml @@ -98,6 +98,10 @@ spec: default: false type: boolean description: 'Whether to setup automatic state and plan encryption for currently unencrypted state. This is only temporarily useful when migrating from an unencrypted state.' + auto_delete_state: + default: false + type: boolean + description: 'Whether to automatically delete the Terraform state. This only makes sense when using the GitLab-managed state backend. It is equivalent to running the delete-state job.' --- @@ -128,3 +132,8 @@ spec: name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]' script: - gitlab-tofu apply -destroy + - | + if $[[ inputs.auto_delete_state ]]; then + echo "Deleting state because auto_delete_state is set to true" + gitlab-tofu delete-state + fi diff --git a/tests/integration-tests/DestroyWithAutoDeleteState.gitlab-ci.yml b/tests/integration-tests/DestroyWithAutoDeleteState.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..f8c75ca4dafc30cb2936bb3a7ebccc59d185ee1b --- /dev/null +++ b/tests/integration-tests/DestroyWithAutoDeleteState.gitlab-ci.yml @@ -0,0 +1,45 @@ +include: + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/apply@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + opentofu_version: $OPENTOFU_VERSION + as: 'setup:apply' + stage: setup + root_dir: $TEST_GITLAB_TOFU_ROOT_DIR + state_name: $TEST_GITLAB_TOFU_STATE_NAME + no_plan: true + # Required to run everything immediately, instead of manually. + rules: [{when: always}] + + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/destroy@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + opentofu_version: $OPENTOFU_VERSION + root_dir: $TEST_GITLAB_TOFU_ROOT_DIR + state_name: $TEST_GITLAB_TOFU_STATE_NAME + stage: destroy + # Required to run everything immediately, instead of manually. + rules: [{when: always}] + # NOTE: we test this setting here + auto_delete_state: true + +stages: [setup, destroy, verify, cleanup] + +verify:destroy-job:did-delete-state: + stage: verify + needs: ['destroy'] + rules: [{when: always}] + image: alpine:latest + before_script: + - apk add --update curl jq + script: + - backend_address="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/$TEST_GITLAB_TOFU_STATE_NAME" + - | + if curl --fail-with-body --silent -u "gitlab-ci-token:${CI_JOB_TOKEN}" "$backend_address"; then + echo 'Error: the state still exists.' + exit 1 + else + echo 'Success: the state is deleted.' + fi diff --git a/tests/integration-tests/DestroyWithoutAutoDeleteState.gitlab-ci.yml b/tests/integration-tests/DestroyWithoutAutoDeleteState.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..6fbbfc639d47d885bb94c8787666351eb9d2509a --- /dev/null +++ b/tests/integration-tests/DestroyWithoutAutoDeleteState.gitlab-ci.yml @@ -0,0 +1,46 @@ +include: + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/apply@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + opentofu_version: $OPENTOFU_VERSION + as: 'setup:apply' + stage: setup + root_dir: $TEST_GITLAB_TOFU_ROOT_DIR + state_name: $TEST_GITLAB_TOFU_STATE_NAME + no_plan: true + # Required to run everything immediately, instead of manually. + rules: [{when: always}] + + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/destroy@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + opentofu_version: $OPENTOFU_VERSION + root_dir: $TEST_GITLAB_TOFU_ROOT_DIR + state_name: $TEST_GITLAB_TOFU_STATE_NAME + stage: destroy + # Required to run everything immediately, instead of manually. + rules: [{when: always}] + # NOTE: this is the default! + # auto_delete_state: false + + # For CI Terraform state cleanup + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/delete-state@$CI_COMMIT_SHA + inputs: + stage: cleanup + state_name: $TEST_GITLAB_TOFU_STATE_NAME + rules: [{when: always}] + +stages: [setup, destroy, verify, cleanup] + +verify:destroy-job:did-not-delete-state: + stage: verify + needs: ['destroy'] + rules: [{when: always}] + image: alpine:latest + before_script: + - apk add --update curl jq + script: + - backend_address="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/$TEST_GITLAB_TOFU_STATE_NAME" + - curl --fail-with-body --silent -u "gitlab-ci-token:${CI_JOB_TOKEN}" "$backend_address" diff --git a/tests/integration-tests/WarningOnNonEmptyPlan.gitlab-ci.yml b/tests/integration-tests/WarningOnNonEmptyPlan.gitlab-ci.yml index 9ec1043ea260ef05f8db0f1f2bdf7e0920424904..1edddd7d70364a78abb67d8b4381025e72d17b83 100644 --- a/tests/integration-tests/WarningOnNonEmptyPlan.gitlab-ci.yml +++ b/tests/integration-tests/WarningOnNonEmptyPlan.gitlab-ci.yml @@ -26,7 +26,6 @@ verify:plan-job:has-warning-state: - apk add --update curl jq script: - | - backend_address="${GITLAB_TOFU_STATE_ADDRESS:-${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${backend_state_name}}" endpoint="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs" is_warning_job=$(curl --silent "$endpoint" | jq -r '.[] | select(.name == "plan") | [.status == "failed", .allow_failure == true] | all') if [ "$is_warning_job" != 'true' ]; then diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml index 92e1012590b03abda8d1938f52addf228220f4c7..c9298a573791c160dc6e1af085ba4bf823ea5ef4 100644 --- a/tests/integration.gitlab-ci.yml +++ b/tests/integration.gitlab-ci.yml @@ -140,3 +140,21 @@ apply-job-template: GITLAB_OPENTOFU_BASE_IMAGE_OS: - alpine - debian + +destroy-job-template: + stage: test-integration + variables: + OPENTOFU_VERSION: $LATEST_OPENTOFU_VERSION + TEST_GITLAB_TOFU_STATE_NAME: ci-integration-$CI_JOB_NAME_SLUG-$CI_PIPELINE_IID-$CI_NODE_INDEX + TEST_GITLAB_TOFU_ROOT_DIR: tests/iac + trigger: + include: tests/integration-tests/$PIPELINE_NAME.gitlab-ci.yml + strategy: depend + parallel: + matrix: + - PIPELINE_NAME: + - DestroyWithAutoDeleteState + - DestroyWithoutAutoDeleteState + GITLAB_OPENTOFU_BASE_IMAGE_OS: + - alpine + - debian