diff --git a/.gitlab/README.md.template b/.gitlab/README.md.template index 520248f81b5cdbfe8ab703deec2ff1331f0c642b..2120dba08024c808a71cb3853c909daa041965e8 100644 --- a/.gitlab/README.md.template +++ b/.gitlab/README.md.template @@ -105,6 +105,7 @@ but no destructive actions. - [`validate-plan`](templates/validate-plan.yml) - [`validate-plan-apply`](templates/validate-plan-apply.yml) +- [`validate-plan-destroy`](templates/validate-plan-destroy.yml) ### Job Templates diff --git a/README.md b/README.md index fb79a6165ef33ce7b3d9738bcf0d216834ee9632..44a22673162cd066e52450e75c70f1a0d3d68de5 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ but no destructive actions. - [`validate-plan`](templates/validate-plan.yml) - [`validate-plan-apply`](templates/validate-plan-apply.yml) +- [`validate-plan-destroy`](templates/validate-plan-destroy.yml) ### Job Templates diff --git a/src/gitlab-tofu.sh b/src/gitlab-tofu.sh index d168c5814b4defaa1791f53bc275c465bb29a203..0f8b02b5949b2822524cd6149de483a51ccb2d5a 100644 --- a/src/gitlab-tofu.sh +++ b/src/gitlab-tofu.sh @@ -16,6 +16,10 @@ if [ -z "$TF_FF_AUTO_URLENCODE_STATE_NAME" ]; then TF_FF_AUTO_URLENCODE_STATE_NAME=true fi +if [ -z "$TF_FF_AUTO_APPROVE_APPLY" ]; then + TF_FF_AUTO_APPROVE_APPLY=true +fi + # Helpers # Evaluate if this script is being sourced or executed directly. @@ -62,25 +66,33 @@ if [ -n "${TF_STATE_NAME}" ] && [ -z "${TF_ADDRESS}" ]; then TF_ADDRESS="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}" fi +if [ -z "${TF_PLAN_NAME}" ]; then + TF_PLAN_NAME=plan +fi + +if [ -z "${TF_APPLY_NO_PLAN}" ]; then + TF_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}/plan.cache" - default_tf_plan_json="${abs_tf_root}/plan.json" + default_tf_plan_cache="${abs_tf_root}/${TF_PLAN_NAME}.cache" + default_tf_plan_json="${abs_tf_root}/${TF_PLAN_NAME}.json" fi # If TF_PLAN_CACHE is not set then use either the plan.cache file within TF_ROOT if set, or plan.cache in CWD if [ -z "${TF_PLAN_CACHE}" ]; then - TF_PLAN_CACHE="${default_tf_plan_cache:-plan.cache}" + TF_PLAN_CACHE="${default_tf_plan_cache:-${TF_PLAN_NAME}.cache}" fi # If TF_PLAN_JSON is not set then use either the plan.json file within TF_ROOT if set, or plan.json in CWD if [ -z "${TF_PLAN_JSON}" ]; then - TF_PLAN_JSON="${default_tf_plan_json:-plan.json}" + TF_PLAN_JSON="${default_tf_plan_json:-${TF_PLAN_NAME}.json}" fi # Set variables for the HTTP backend to default to TF_* values @@ -157,8 +169,17 @@ if [ $sourced -eq 0 ]; then case "${1}" in "apply") + auto_approve_args="" + if [ "${TF_FF_AUTO_APPROVE_APPLY}" = true ]; then + auto_approve_args="-auto-approve" + fi + $TF_IMPLICIT_INIT && terraform_init - tofu "${TF_CHDIR_OPT}" "${@}" -input=false "${TF_PLAN_CACHE}" + if [ "$TF_APPLY_NO_PLAN" = false ]; then + tofu "${TF_CHDIR_OPT}" "${@}" -input=false "${auto_approve_args}" "${TF_PLAN_CACHE}" + else + tofu "${TF_CHDIR_OPT}" "${@}" -input=false "${auto_approve_args}" + fi ;; "destroy") $TF_IMPLICIT_INIT && terraform_init diff --git a/templates/apply.yml b/templates/apply.yml index b4c321f7bea6712d33c937e02a8771ee3456c358..e2d37785e6a28c5aa3c51d7e1e64c37ed004d1b1 100644 --- a/templates/apply.yml +++ b/templates/apply.yml @@ -50,6 +50,13 @@ spec: state_name: default: default description: 'Remote OpenTofu state name.' + no_plan: + default: false + type: boolean + description: 'Whether a plan file should be used.' + plan_name: + default: 'plan' + description: 'The name of the plan file to use. Will be used for TF_PLAN_CACHE and TF_PLAN_JSON.' auto_apply: default: false type: boolean @@ -76,6 +83,8 @@ spec: __CACHE_KEY_HACK: "$[[ inputs.root_dir ]]" TF_ROOT: $[[ inputs.root_dir ]] TF_STATE_NAME: $[[ inputs.state_name ]] + TF_APPLY_NO_PLAN: $[[ inputs.no_plan ]] + TF_PLAN_NAME: $[[ inputs.plan_name ]] image: name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]' script: diff --git a/templates/destroy.yml b/templates/destroy.yml index 97cb73657121471871e355e5b9828c7c87de67dd..cd9a743184f27352aac9d355f297f24645db7c47 100644 --- a/templates/destroy.yml +++ b/templates/destroy.yml @@ -50,6 +50,13 @@ spec: state_name: default: default description: 'Remote OpenTofu state name.' + no_plan: + default: true + type: boolean + description: 'Whether a plan file should be used.' + plan_name: + default: 'destroy-plan' + description: 'The name of the plan file to use. Will be used for TF_PLAN_CACHE and TF_PLAN_JSON.' auto_destroy: default: false type: boolean @@ -75,7 +82,9 @@ spec: __CACHE_KEY_HACK: "$[[ inputs.root_dir ]]" TF_ROOT: $[[ inputs.root_dir ]] TF_STATE_NAME: $[[ inputs.state_name ]] + TF_APPLY_NO_PLAN: $[[ inputs.no_plan ]] + TF_PLAN_NAME: $[[ inputs.plan_name ]] image: name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]' script: - - gitlab-tofu destroy + - gitlab-tofu apply -destroy diff --git a/templates/plan.yml b/templates/plan.yml index 7441ebddd57b6037a1ecdf0860119b0a610d3e98..a28b9c614cf3439d695691141beacf8a40ddcfbf 100644 --- a/templates/plan.yml +++ b/templates/plan.yml @@ -50,6 +50,13 @@ spec: state_name: default: default description: 'Remote OpenTofu state name.' + plan_name: + default: 'plan' + description: 'The name of the plan cache and plan json file.' + destroy: + default: false + type: boolean + description: 'Indicate if the plan should be a destroy plan. You may want to change the `plan_name` input to `destroy-plan` which is the default for the destroy job.' --- @@ -68,9 +75,9 @@ spec: # See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic public: false paths: - - $TF_ROOT/plan.cache + - $TF_ROOT/$[[ inputs.plan_name ]].cache reports: - terraform: $TF_ROOT/plan.json + terraform: $TF_ROOT/$[[ inputs.plan_name]].json rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline. @@ -85,8 +92,15 @@ spec: __CACHE_KEY_HACK: "$[[ inputs.root_dir ]]" TF_ROOT: $[[ inputs.root_dir ]] TF_STATE_NAME: $[[ inputs.state_name ]] + TF_PLAN_NAME: $[[ inputs.plan_name ]] image: name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]' script: - - gitlab-tofu plan + - | + args="" + if [ "$[[ inputs.destroy ]]" == "true" ]; then + echo "Planning for a destroy" + args="-destroy" + fi + - gitlab-tofu plan $args - gitlab-tofu plan-json diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml new file mode 100644 index 0000000000000000000000000000000000000000..b94fc546fab0e98a9456fa4df5b7b354669a86ac --- /dev/null +++ b/templates/validate-plan-destroy.yml @@ -0,0 +1,119 @@ +spec: + inputs: + # Stages + stage_validate: + default: 'validate' + description: 'Defines the validate stage. This stage includes the `fmt` and `validate` jobs.' + stage_build: + default: 'build' + description: 'Defines the build stage. This stage includes the `plan` job.' + stage_cleanup: + default: 'cleanup' + description: 'Defines the cleanup stage. This stage includes the `destroy` and `delete-state` jobs.' + + # Versions + # This version is only required, because we cannot access the context of the component, + # see https://gitlab.com/gitlab-org/gitlab/-/issues/438275 + version: + default: 'latest' + description: 'Version of this component. Has to be the same as the one in the component include entry.' + + opentofu_version: + default: '1.7.1' + options: + - '$OPENTOFU_VERSION' + - '1.7.1' + - '1.7.0' + - '1.7.0-alpha1' + - '1.6.2' + - '1.6.1' + - '1.6.0' + description: 'OpenTofu version that should be used.' + + # Images + image_registry_base: + default: '$CI_REGISTRY/components/opentofu' + description: 'Host URI to the job images. Will be combined with `image_name` to construct the actual image URI.' + # FIXME: not yet possible because of https://gitlab.com/gitlab-org/gitlab/-/issues/438722 + # gitlab_opentofu_image: + # # FIXME: This should reference the component tag that is used. + # # Currently, blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/438275 + # # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.opentofu_version ]]' + # default: '$CI_REGISTRY/components/opentofu/gitlab-opentofu:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]' + # description: 'Tag of the gitlab-opentofu image.' + + image_name: + default: 'gitlab-opentofu' + description: 'Image name for the job images. Hosted under `image_registry_base`.' + + # Configuration + root_dir: + default: ${CI_PROJECT_DIR} + description: 'Root directory for the OpenTofu project.' + state_name: + default: default + description: 'Remote OpenTofu state name.' + plan_name: + default: 'destroy-plan' + description: 'Destroy plan file name.' + auto_destroy: + default: false + type: boolean + description: 'Whether the destroy job is manual or automatically run.' + +--- + +include: + - local: '/templates/fmt.yml' + inputs: + as: 'fmt' + stage: $[[ inputs.stage_validate ]] + version: $[[ inputs.version ]] + opentofu_version: $[[ inputs.opentofu_version ]] + image_registry_base: $[[ inputs.image_registry_base ]] + image_name: $[[ inputs.image_name ]] + root_dir: $[[ inputs.root_dir ]] + - local: '/templates/validate.yml' + inputs: + as: 'validate' + stage: $[[ inputs.stage_validate ]] + version: $[[ inputs.version ]] + opentofu_version: $[[ inputs.opentofu_version ]] + image_registry_base: $[[ inputs.image_registry_base ]] + image_name: $[[ inputs.image_name ]] + root_dir: $[[ inputs.root_dir ]] + state_name: $[[ inputs.state_name ]] + - local: '/templates/plan.yml' + inputs: + as: 'plan' + stage: $[[ inputs.stage_build ]] + version: $[[ inputs.version ]] + opentofu_version: $[[ inputs.opentofu_version ]] + image_registry_base: $[[ inputs.image_registry_base ]] + image_name: $[[ inputs.image_name ]] + root_dir: $[[ inputs.root_dir ]] + state_name: $[[ inputs.state_name ]] + plan_name: $[[ inputs.plan_name ]] + destroy: true + - local: '/templates/destroy.yml' + inputs: + as: 'destroy' + stage: $[[ inputs.stage_cleanup ]] + version: $[[ inputs.version ]] + opentofu_version: $[[ inputs.opentofu_version ]] + image_registry_base: $[[ inputs.image_registry_base ]] + image_name: $[[ inputs.image_name ]] + root_dir: $[[ inputs.root_dir ]] + state_name: $[[ inputs.state_name ]] + no_plan: false + plan_name: $[[ inputs.plan_name ]] + auto_destroy: $[[ inputs.auto_destroy ]] + - local: '/templates/delete-state.yml' + inputs: + as: 'delete-state' + stage: $[[ inputs.stage_cleanup ]] + state_name: $[[ inputs.state_name ]] + +# NOTE: we have to define this `needs` here, because inputs don't support arrays, yet. +delete-state: + needs: [destroy] diff --git a/tests/integration-tests/Destroy.gitlab-ci.yml b/tests/integration-tests/Destroy.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0ec3c56ae349723dedf34cdfd13d4561621ec08 --- /dev/null +++ b/tests/integration-tests/Destroy.gitlab-ci.yml @@ -0,0 +1,43 @@ +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_TF_ROOT + state_name: $TEST_TF_STATE_NAME + no_plan: true + auto_apply: true + + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/validate-plan-destroy@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + opentofu_version: $OPENTOFU_VERSION + root_dir: $TEST_TF_ROOT + state_name: $TEST_TF_STATE_NAME + +stages: [setup, validate, build, cleanup] + +# Required to run everything immediately, instead of manually. + +'setup:apply': + rules: [{when: always}] + +fmt: + rules: [{when: always}] + +validate: + rules: [{when: always}] + +plan: + rules: [{when: always}] + +destroy: + rules: [{when: always}] + +delete-state: + rules: [{when: always}] + diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml index 8f490d7441e320cd8ed19329c3e3e86bc231a7c4..ca0369b60c6531be1b47a082ae0b61b0b39213cf 100644 --- a/tests/integration.gitlab-ci.yml +++ b/tests/integration.gitlab-ci.yml @@ -12,3 +12,4 @@ component: - PIPELINE_NAME: [Defaults] - PIPELINE_NAME: [JobTemplates] - PIPELINE_NAME: [TestJob] + - PIPELINE_NAME: [Destroy]