diff --git a/src/gitlab-tofu.sh b/src/gitlab-tofu.sh index b552713ca1c12670fc5095061fe138533dd86eef..1d4eb464f26e68a1a956a64f77aedc3024a8131d 100644 --- a/src/gitlab-tofu.sh +++ b/src/gitlab-tofu.sh @@ -237,9 +237,14 @@ if [ $sourced -eq 0 ]; then terraform_init "${@}" ;; "plan") + plan_args='' + if [ "${OPENTOFU_COMPONENT_USE_DETAILED_EXITCODE}" = 'true' ]; then + plan_args='-detailed-exitcode' + fi + $TF_IMPLICIT_INIT && terraform_init # shellcheck disable=SC2086 - tofu "${TF_CHDIR_OPT}" "${@}" -input=false -out="${TF_PLAN_CACHE}" ${var_file_args} + tofu "${TF_CHDIR_OPT}" "${@}" -input=false -out="${TF_PLAN_CACHE}" ${var_file_args} ${plan_args} ;; "plan-json") tofu "${TF_CHDIR_OPT}" show -json "${TF_PLAN_CACHE}" | \ diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml index 477e3c576c66d99648edaac626f7b0fa70881036..65a219545b307a5ec4f7a20470a2d9c3824713ab 100644 --- a/templates/full-pipeline.yml +++ b/templates/full-pipeline.yml @@ -155,6 +155,10 @@ spec: - when: manual type: array description: 'Defines the `rules` of the `delete-state` job.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- @@ -220,6 +224,7 @@ include: artifacts_access: $[[ inputs.plan_artifacts_access ]] var_file: $[[ inputs.var_file ]] rules: $[[ inputs.plan_rules ]] + warning_on_none_empty_plan: $[[ inputs.warning_on_none_empty_plan ]] - local: '/templates/apply.yml' inputs: as: '$[[ inputs.job_name_prefix ]]apply' @@ -262,5 +267,5 @@ include: # job template, but the issue is that we cannot default it to something # meaningful other than `null` - but `null` is also not yet supported, see # https://gitlab.com/gitlab-org/gitlab/-/issues/440468 -$[[ inputs.job_name_prefix ]]delete-state: +'$[[ inputs.job_name_prefix ]]delete-state': needs: ['$[[ inputs.job_name_prefix ]]destroy'] diff --git a/templates/job-templates.yml b/templates/job-templates.yml index f4602c9d91bfa54eed7123e70acaff56369583e7..69120b5eb4cb187456ffb418710180662c966af3 100644 --- a/templates/job-templates.yml +++ b/templates/job-templates.yml @@ -101,6 +101,10 @@ spec: default: '' type: string description: 'Path to a variables files relative to root_dir.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- @@ -167,6 +171,7 @@ include: state_name: $[[ inputs.state_name ]] plan_name: $[[ inputs.plan_name ]] var_file: $[[ inputs.var_file ]] + warning_on_none_empty_plan: $[[ inputs.warning_on_none_empty_plan ]] - local: '/templates/apply.yml' inputs: as: '$[[ inputs.job_name_prefix ]]apply' diff --git a/templates/plan.yml b/templates/plan.yml index b08502ab78490f2bf3c66b23177cef59132702b1..c10d2d36b04fdd9c8afa88deaa0a3ef5b29c2a72 100644 --- a/templates/plan.yml +++ b/templates/plan.yml @@ -99,11 +99,31 @@ spec: default: pull-push type: string description: 'Defines the cache policy of the job.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- +# NOTE: the two following jobs are necessary to implement the abstraction logic +# required for the `warning_on_none_empty_plan` input. +# Without any kind of flow control support for the GitLab CI YAML we cannot infer +# another value from the input. However, we can clearly apply "inheritance" to +# customize behavior related to the CI keywords that have otherwise nothing +# in common with the inputs value. +'.$[[ inputs.as ]]:detailed_exitcode:warning:false': + extends: null + +'.$[[ inputs.as ]]:detailed_exitcode:warning:true': + allow_failure: + exit_codes: [2] + '$[[ inputs.as ]]': stage: $[[ inputs.stage ]] + extends: + # NOTE: see the comment above. This is to support the `warning_on_none_empty_plan` input. + - '.$[[ inputs.as ]]:detailed_exitcode:warning:$[[ inputs.warning_on_none_empty_plan ]]' environment: name: $[[ inputs.state_name ]] action: prepare @@ -131,6 +151,11 @@ spec: TF_STATE_NAME: $[[ inputs.state_name ]] TF_PLAN_NAME: $[[ inputs.plan_name ]] OPENTOFU_COMPONENT_VAR_FILE: '$[[ inputs.var_file ]]' + OPENTOFU_COMPONENT_USE_DETAILED_EXITCODE: '$[[ inputs.warning_on_none_empty_plan ]]' + # NOTE: we rely on correct exitcode reporting behavior for the `warning_on_none_empty_plan` input + # behavior. However, when using bash the runner does not work properly without setting + # the feature flag below to `true`. + FF_USE_NEW_BASH_EVAL_STRATEGY: 'true' image: name: '$[[ inputs.image_registry_base ]]/$[[ inputs.image_name ]]:$[[ inputs.version ]]-opentofu$[[ inputs.opentofu_version ]]-$[[ inputs.base_os ]]$[[ inputs.image_digest ]]' script: diff --git a/templates/validate-plan-apply.yml b/templates/validate-plan-apply.yml index 8fd8a925e37d8644d3b1cae5ff05b0dbedaa00fe..9b9a81adc38b7d17a7e3b053c8702a2ee26a5ca0 100644 --- a/templates/validate-plan-apply.yml +++ b/templates/validate-plan-apply.yml @@ -125,6 +125,10 @@ spec: when: manual type: array description: 'Defines the `rules` of the `apply` job.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- @@ -172,6 +176,7 @@ include: artifacts_access: $[[ inputs.plan_artifacts_access ]] var_file: $[[ inputs.var_file ]] rules: $[[ inputs.plan_rules ]] + warning_on_none_empty_plan: $[[ inputs.warning_on_none_empty_plan ]] - local: '/templates/apply.yml' inputs: as: '$[[ inputs.job_name_prefix ]]apply' diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml index 7e81b35661dbcd1f7fcc61f983c7e29a4c44ba26..cc5ceb4939d851ee003d3322871b0ee4093d8022 100644 --- a/templates/validate-plan-destroy.yml +++ b/templates/validate-plan-destroy.yml @@ -130,6 +130,10 @@ spec: - when: manual type: array description: 'Defines the `rules` of the `delete-state` job.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- @@ -178,6 +182,7 @@ include: destroy: true var_file: $[[ inputs.var_file ]] rules: $[[ inputs.plan_rules ]] + warning_on_none_empty_plan: $[[ inputs.warning_on_none_empty_plan ]] - local: '/templates/destroy.yml' inputs: as: '$[[ inputs.job_name_prefix ]]destroy' @@ -206,5 +211,5 @@ include: # job template, but the issue is that we cannot default it to something # meaningful other than `null` - but `null` is also not yet supported, see # https://gitlab.com/gitlab-org/gitlab/-/issues/440468 -$[[ inputs.job_name_prefix ]]delete-state: +'$[[ inputs.job_name_prefix ]]delete-state': needs: ['$[[ inputs.job_name_prefix ]]destroy'] diff --git a/templates/validate-plan.yml b/templates/validate-plan.yml index a491411b66c87b3838e4da14f41fea20cdc9881f..feeba5a1e229efefab8f939810c01de537080cb4 100644 --- a/templates/validate-plan.yml +++ b/templates/validate-plan.yml @@ -111,6 +111,10 @@ spec: - if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead. type: array description: 'Defines the `rules` of the `plan` job.' + warning_on_none_empty_plan: + default: false + type: boolean + description: 'Whether to mark the job with a warning if the plan contains a diff.' --- @@ -158,3 +162,4 @@ include: artifacts_access: $[[ inputs.artifacts_access ]] var_file: $[[ inputs.var_file ]] rules: $[[ inputs.plan_rules ]] + warning_on_none_empty_plan: $[[ inputs.warning_on_none_empty_plan ]] diff --git a/tests/integration-tests/WarningOnNoneEmptyPlan.gitlab-ci.yml b/tests/integration-tests/WarningOnNoneEmptyPlan.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..1c3fbf5ef9d2685d28aa2c98dcf00e97d09fddfa --- /dev/null +++ b/tests/integration-tests/WarningOnNoneEmptyPlan.gitlab-ci.yml @@ -0,0 +1,36 @@ +include: + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/plan@$CI_COMMIT_SHA + inputs: + image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE + version: $CI_COMMIT_SHA + base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS + opentofu_version: $OPENTOFU_VERSION + root_dir: $TEST_TF_ROOT + state_name: $TEST_TF_STATE_NAME + warning_on_none_empty_plan: true + + # For CI Terraform state cleanup + - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/delete-state@$CI_COMMIT_SHA + inputs: + state_name: $TEST_TF_STATE_NAME + rules: [{when: always}] + +stages: [build, cleanup, verify] + +verify:plan-job:has-warning-state: + stage: verify + needs: ['plan'] + rules: [{when: always}] + image: alpine:latest + before_script: + - apk add --update curl jq + script: + - | + 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 + echo 'Error: the plan job was not in warning state.' + exit 1 + else + echo 'Success: the plan job was in warning state.' + fi diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml index af76bcbdec8f316bea3f05fc6bac6a34f7fd49d7..90e8bc1b7a8208b7f188b652a19aefd0f3d04204 100644 --- a/tests/integration.gitlab-ci.yml +++ b/tests/integration.gitlab-ci.yml @@ -16,6 +16,7 @@ component: - ModuleRelease - Destroy - VarFile + - WarningOnNoneEmptyPlan GITLAB_OPENTOFU_BASE_IMAGE_OS: - alpine - debian