diff --git a/.github/workflows/test-refresh.yaml b/.github/workflows/test-refresh.yaml new file mode 100644 index 00000000..589c80e7 --- /dev/null +++ b/.github/workflows/test-refresh.yaml @@ -0,0 +1,187 @@ +name: Test terraform-refresh + +on: + - pull_request + +permissions: + contents: read + +jobs: + refresh: + runs-on: ubuntu-24.04 + name: Refresh + permissions: + contents: read + pull-requests: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Apply + uses: ./terraform-apply + with: + path: tests/workflows/test-refresh + auto_approve: true + + - name: Check no changes + uses: ./terraform-check + with: + path: tests/workflows/test-refresh + + - name: Make untracked changes + run: | + echo "qewasdasd" > tests/workflows/test-refresh/test + echo "cxvbbxcbb" > tests/workflows/test-refresh/test2 + echo "tyuityuiy" > tests/workflows/test-refresh/test3 + + - name: Create a normal plan + uses: ./terraform-plan + id: plan-with-refresh + with: + add_github_comment: false + path: tests/workflows/test-refresh + + - name: Check normal plan picks up changes + env: + CHANGES: ${{ steps.plan-with-refresh.outputs.changes }} + TO_ADD: ${{ steps.plan-with-refresh.outputs.to_add }} + run: | + if [[ "$CHANGES" != "true" ]]; then + echo "::error:: Plan did not have changes" + exit 1 + fi + + if [[ "$TO_ADD" != "3" ]]; then + echo "::error:: Wrong number of resources to add" + exit 1 + fi + + - name: Create a non-refresh plan + uses: ./terraform-plan + id: plan-without-refresh + with: + add_github_comment: false + label: test-refresh refresh non-refresh + path: tests/workflows/test-refresh + refresh: false + + - name: Check non-refresh plan doesn't pick up changes + env: + CHANGES: ${{ steps.plan-without-refresh.outputs.changes }} + TO_ADD: ${{ steps.plan-without-refresh.outputs.to_add }} + run: | + if [[ "$CHANGES" != "false" ]]; then + echo "::error:: Plan has changes" + exit 1 + fi + + - name: Targeted refresh + uses: ./terraform-refresh + with: + path: tests/workflows/test-refresh + target: | + local_file.one + + - name: Plan after targeted refresh + uses: ./terraform-plan + id: plan-after-targeted-refresh + with: + path: tests/workflows/test-refresh + refresh: false + + - name: Check plan after targeted refresh + env: + CHANGES: ${{ steps.plan-after-targeted-refresh.outputs.changes }} + TO_ADD: ${{ steps.plan-after-targeted-refresh.outputs.to_add }} + run: | + if [[ "$CHANGES" != "true" ]]; then + echo "::error:: Plan did not have changes" + exit 1 + fi + + if [[ "$TO_ADD" != "1" ]]; then + echo "::error:: Wrong number of resources to add" + exit 1 + fi + + - name: Apply plan after targeted refresh + uses: ./terraform-apply + id: apply + continue-on-error: true + with: + path: tests/workflows/test-refresh + + - name: Check failed to apply + env: + OUTCOME: ${{ steps.apply.outcome }} + FAILURE_REASON: ${{ steps.apply.outputs.failure-reason }} + run: | + if [[ "$OUTCOME" != "failure" ]]; then + echo "Apply did not fail correctly" + exit 1 + fi + + if [[ "$FAILURE_REASON" != "plan-changed" ]]; then + echo "::error:: failure-reason not set correctly" + exit 1 + fi + + - name: Apply without refresh + uses: ./terraform-apply + with: + path: tests/workflows/test-refresh + refresh: false + + - name: Create another normal plan + uses: ./terraform-plan + id: plan-with-refresh-after-apply + with: + add_github_comment: false + path: tests/workflows/test-refresh + + - name: Check normal plan picks up changes + env: + CHANGES: ${{ steps.plan-with-refresh-after-apply.outputs.changes }} + TO_ADD: ${{ steps.plan-with-refresh-after-apply.outputs.to_add }} + run: | + if [[ "$CHANGES" != "true" ]]; then + echo "::error:: Plan did not have changes" + exit 1 + fi + + if [[ "$TO_ADD" != "2" ]]; then + echo "::error:: Wrong number of resources to add" + exit 1 + fi + + - name: Full refresh + uses: ./terraform-refresh + with: + path: tests/workflows/test-refresh + + - name: Plan after full refresh + uses: ./terraform-plan + id: plan-after-full-refresh + with: + add_github_comment: false + path: tests/workflows/test-refresh + refresh: false + + - name: Check plan after full refresh + env: + CHANGES: ${{ steps.plan-after-full-refresh.outputs.changes }} + TO_ADD: ${{ steps.plan-after-full-refresh.outputs.to_add }} + run: | + if [[ "$CHANGES" != "true" ]]; then + echo "::error:: Plan did not have changes" + exit 1 + fi + + if [[ "$TO_ADD" != "2" ]]; then + echo "::error:: Wrong number of resources to add" + exit 1 + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 64bcf4c2..d14d0fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,22 @@ The actions are versioned as a suite. Some actions may have no change in behavio When using an action you can specify the version as: -- `@v1.47.0` to use an exact release -- `@v1.47` to use the latest patch release for the specific minor version +- `@v1.48.0` to use an exact release +- `@v1.48` to use the latest patch release for the specific minor version - `@v1` to use the latest patch release for the specific major version +## [1.48.0] - 2025-03-24 + +### Added +- A `refresh` input for [dflook/terraform-plan](https://github.com/dflook/terraform-github-actions/tree/main/terraform-plan)/[tofu-plan](https://github.com/dflook/terraform-github-actions/tree/main/tofu-plan) + and [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/tree/main/terraform-apply)/[tofu-apply](https://github.com/dflook/terraform-github-actions/tree/main/tofu-apply) + + This defaults to `true` with the current behaviour of refreshing the state before planning or applying. + When set to `false` the state will not be refreshed, which can be a lot faster but may result in an outdated plan. + +- New [dflook/terraform-refresh](https://github.com/dflook/terraform-github-actions/tree/main/terraform-refresh)/[tofu-refresh](https://github.com/dflook/terraform-github-actions/tree/main/tofu-refresh) + actions to update the state file to match the current state of the infrastructure, but doesn't make any changes to the infrastructure. + ## [1.47.0] - 2025-02-28 ### Added @@ -724,6 +736,7 @@ First release of the GitHub Actions: - [dflook/terraform-new-workspace](terraform-new-workspace) - [dflook/terraform-destroy-workspace](terraform-destroy-workspace) +[1.48.0]: https://github.com/dflook/terraform-github-actions/compare/v1.47.0...v1.48.0 [1.47.0]: https://github.com/dflook/terraform-github-actions/compare/v1.46.1...v1.47.0 [1.46.1]: https://github.com/dflook/terraform-github-actions/compare/v1.46.0...v1.46.1 [1.46.0]: https://github.com/dflook/terraform-github-actions/compare/v1.45.0...v1.46.0 diff --git a/README.md b/README.md index e53b88ae..a67b65a5 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,23 @@ Currently, there is just experimental support for OpenTofu, see [here](https://g See the documentation for the available actions: -| Terraform | OpenTofu | -|--------------------------------------------------------------------|---------------------------------------------------------| -| [dflook/terraform-plan](terraform-plan) | [dflook/tofu-plan](tofu-plan) | -| [dflook/terraform-apply](terraform-apply) | [dflook/tofu-apply](tofu-apply) | -| [dflook/terraform-output](terraform-output) | [dflook/tofu-output](tofu-output) | -| [dflook/terraform-remote-state](terraform-remote-state) | [dflook/tofu-remote-state](tofu-remote-state) | -| [dflook/terraform-validate](terraform-validate) | [dflook/tofu-validate](tofu-validate) | -| [dflook/terraform-fmt-check](terraform-fmt-check) | [dflook/tofu-fmt-check](tofu-fmt-check) | -| [dflook/terraform-fmt](terraform-fmt) | [dflook/tofu-fmt](tofu-fmt) | -| [dflook/terraform-check](terraform-check) | [dflook/tofu-check](tofu-check) | -| [dflook/terraform-new-workspace](terraform-new-workspace) | [dflook/tofu-new-workspace](tofu-new-workspace) | -| [dflook/terraform-destroy-workspace](terraform-destroy-workspace) | [dflook/tofu-destroy-workspace](tofu-destroy-workspace) | -| [dflook/terraform-destroy](terraform-destroy) | [dflook/tofu-destroy](tofu-destroy) | -| [dflook/terraform-version](terraform-version) | [dflook/tofu-version](tofu-version) | -| [dflook/terraform-unlock-state](terraform-unlock-state) | [dflook/tofu-unlock-state](tofu-unlock-state) | -| [dflook/terraform-test](terraform-test) | [dflook/tofu-test](tofu-test) | +| Terraform | OpenTofu | +|-------------------------------------------------------------------|---------------------------------------------------------| +| [dflook/terraform-plan](terraform-plan) | [dflook/tofu-plan](tofu-plan) | +| [dflook/terraform-apply](terraform-apply) | [dflook/tofu-apply](tofu-apply) | +| [dflook/terraform-output](terraform-output) | [dflook/tofu-output](tofu-output) | +| [dflook/terraform-remote-state](terraform-remote-state) | [dflook/tofu-remote-state](tofu-remote-state) | +| [dflook/terraform-validate](terraform-validate) | [dflook/tofu-validate](tofu-validate) | +| [dflook/terraform-fmt-check](terraform-fmt-check) | [dflook/tofu-fmt-check](tofu-fmt-check) | +| [dflook/terraform-fmt](terraform-fmt) | [dflook/tofu-fmt](tofu-fmt) | +| [dflook/terraform-check](terraform-check) | [dflook/tofu-check](tofu-check) | +| [dflook/terraform-new-workspace](terraform-new-workspace) | [dflook/tofu-new-workspace](tofu-new-workspace) | +| [dflook/terraform-destroy-workspace](terraform-destroy-workspace) | [dflook/tofu-destroy-workspace](tofu-destroy-workspace) | +| [dflook/terraform-destroy](terraform-destroy) | [dflook/tofu-destroy](tofu-destroy) | +| [dflook/terraform-version](terraform-version) | [dflook/tofu-version](tofu-version) | +| [dflook/terraform-unlock-state](terraform-unlock-state) | [dflook/tofu-unlock-state](tofu-unlock-state) | +| [dflook/terraform-test](terraform-test) | [dflook/tofu-test](tofu-test) | +| [dflook/terraform-refresh](terraform-refresh) | [dflook/tofu-refresh](tofu-refresh) | ## Example Usage diff --git a/docs-gen/action.py b/docs-gen/action.py index 186a54ac..0dbf45aa 100644 --- a/docs-gen/action.py +++ b/docs-gen/action.py @@ -172,6 +172,7 @@ def assert_ordering(self): "replace", "target", "destroy", + "refresh", "plan_path", "auto_approve", "add_github_comment", diff --git a/docs-gen/actions/apply.py b/docs-gen/actions/apply.py index 757126cf..3ea6f327 100644 --- a/docs-gen/actions/apply.py +++ b/docs-gen/actions/apply.py @@ -16,6 +16,7 @@ from inputs.parallelism import parallelism from inputs.path import path from inputs.plan_path import plan_path as plan_path_input +from inputs.refresh import refresh from inputs.replace import replace from inputs.target import target from inputs.var_file import var_file @@ -83,6 +84,7 @@ Set to `true` to destroy all resources. This generates and applies a plan in [destroy mode]($DestroyModeUrl).'''), + refresh, plan_path_input, auto_approve, parallelism @@ -339,4 +341,4 @@ auto_approve: true ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/plan.py b/docs-gen/actions/plan.py index 3c32628c..67f81946 100644 --- a/docs-gen/actions/plan.py +++ b/docs-gen/actions/plan.py @@ -16,6 +16,7 @@ from inputs.label import label from inputs.parallelism import parallelism from inputs.path import path +from inputs.refresh import refresh from inputs.replace import replace from inputs.target import target from inputs.var import var @@ -57,6 +58,7 @@ replace, target, destroy, + refresh, add_github_comment, parallelism ], @@ -251,4 +253,4 @@ path: my-$ToolName-config ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/refresh.py b/docs-gen/actions/refresh.py new file mode 100644 index 00000000..0150e550 --- /dev/null +++ b/docs-gen/actions/refresh.py @@ -0,0 +1,62 @@ +import dataclasses + +from action import Action +from environment_variables.GITHUB_DOT_COM_TOKEN import GITHUB_DOT_COM_TOKEN +from environment_variables.TERRAFORM_CLOUD_TOKENS import TERRAFORM_CLOUD_TOKENS +from environment_variables.TERRAFORM_HTTP_CREDENTIALS import TERRAFORM_HTTP_CREDENTIALS +from environment_variables.TERRAFORM_PRE_RUN import TERRAFORM_PRE_RUN +from environment_variables.TERRAFORM_SSH_KEY import TERRAFORM_SSH_KEY +from inputs.backend_config import backend_config +from inputs.backend_config_file import backend_config_file +from inputs.parallelism import parallelism +from inputs.path import path +from inputs.var_file import var_file +from inputs.variables import variables +from inputs.workspace import workspace +from inputs.target import target +from outputs.failure_reason import failure_reason +from outputs.lock_info import lock_info +from outputs.run_id import run_id + +refresh = Action( + 'refresh', + ''' +This actions runs a $ProductName apply operation in refresh-only mode. +This will synchronise the $ProductName state with the actual resources, but will not make any changes to the resources. +''', + meta_description='Refresh $ProductName state', + inputs=[ + dataclasses.replace(path, description="Path to the $ProductName root module to refresh state for"), + dataclasses.replace(workspace, description="$ProductName workspace to run the refresh state in"), + variables, + var_file, + backend_config, + backend_config_file, + dataclasses.replace(target, description=''' +List of resources to target, one per line. +The refresh will be limited to these resources and their dependencies. +'''), + parallelism + ], + outputs=[ + dataclasses.replace(failure_reason, description=''' + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The $ProductName apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. + ''' + ), + lock_info, + run_id + ], + environment_variables=[ + GITHUB_DOT_COM_TOKEN, + TERRAFORM_CLOUD_TOKENS, + TERRAFORM_SSH_KEY, + TERRAFORM_HTTP_CREDENTIALS, + TERRAFORM_PRE_RUN, + ] +) diff --git a/docs-gen/generate.py b/docs-gen/generate.py index 4283b104..bf6b3459 100644 --- a/docs-gen/generate.py +++ b/docs-gen/generate.py @@ -8,6 +8,7 @@ from actions.new_workspace import new_workspace from actions.output import output from actions.plan import plan +from actions.refresh import refresh from actions.remote_state import remote_state from actions.test import test from actions.unlock_state import unlock_state @@ -18,7 +19,6 @@ def tofuize(str) -> str: return str.replace('a OpenTofu', 'an OpenTofu') for action in [ - plan, apply, check, destroy, @@ -27,12 +27,13 @@ def tofuize(str) -> str: fmt_check, new_workspace, output, + plan, + refresh, remote_state, test, unlock_state, validate, - version - + version, ]: action.assert_ordering() diff --git a/docs-gen/inputs/refresh.py b/docs-gen/inputs/refresh.py new file mode 100644 index 00000000..3742b190 --- /dev/null +++ b/docs-gen/inputs/refresh.py @@ -0,0 +1,13 @@ +from action import Input + +refresh = Input( + name='refresh', + type='boolean', + description=''' +Set to `false` to skip synchronisation of the $ProductName state with actual resources. + +This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. +''', + required=False, + default='true' +) diff --git a/docs-gen/inputs/target.py b/docs-gen/inputs/target.py index 1c936cf6..657a55fe 100644 --- a/docs-gen/inputs/target.py +++ b/docs-gen/inputs/target.py @@ -4,7 +4,7 @@ name='target', type='string', description=''' -List of resources to apply, one per line. +List of resources to target, one per line. The plan will be limited to these resources and their dependencies. ''', example = ''' diff --git a/image/actions.sh b/image/actions.sh index 46734a34..b0e6f93d 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -360,6 +360,12 @@ function set-common-plan-args() { PLAN_ARGS="$PLAN_ARGS -destroy" fi fi + + if [[ -v INPUT_REFRESH ]]; then + if [[ "$INPUT_REFRESH" == "false" ]]; then + PLAN_ARGS="$PLAN_ARGS -refresh=false" + fi + fi } function set-variable-args() { diff --git a/image/entrypoints/refresh.sh b/image/entrypoints/refresh.sh new file mode 100755 index 00000000..5e1a59fb --- /dev/null +++ b/image/entrypoints/refresh.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# shellcheck source=../actions.sh +source /usr/local/actions.sh + +debug +setup +init-backend-workspace +set-variable-args + +exec 3>&1 + +function refresh() { + local REFRESH_EXIT + + PARALLEL_ARG="" + if [[ "$INPUT_PARALLELISM" -ne 0 ]]; then + PARALLEL_ARG="-parallelism=$INPUT_PARALLELISM" + fi + + REFRESH_ARGS="" + if [[ -v INPUT_TARGET ]]; then + if [[ -n "$INPUT_TARGET" ]]; then + for target in $(echo "$INPUT_TARGET" | tr ',' '\n'); do + REFRESH_ARGS="$REFRESH_ARGS -target $target" + done + fi + fi + + set +e + + # shellcheck disable=SC2086,SC2016 + debug_log $TOOL_COMMAND_NAME refresh -input=false -no-color -lock-timeout=300s $PARALLEL_ARG $REFRESH_ARGS $VARIABLE_ARGS + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME refresh -input=false -no-color -lock-timeout=300s $PARALLEL_ARG $REFRESH_ARGS $VARIABLE_ARGS) \ + 2>"$STEP_TMP_DIR/terraform_refresh.stderr" \ + | tee "$STEP_TMP_DIR/terraform_refresh.stdout" + REFRESH_EXIT=${PIPESTATUS[0]} + >&2 cat "$STEP_TMP_DIR/terraform_refresh.stderr" + + set -e + + if [[ "$TERRAFORM_BACKEND_TYPE" == "cloud" || "$TERRAFORM_BACKEND_TYPE" == "remote" ]]; then + if remote-run-id "$STEP_TMP_DIR/terraform_refresh.stdout" "$STEP_TMP_DIR/terraform_refresh.stderr" >"$STEP_TMP_DIR/remote-run-id.stdout" 2>"$STEP_TMP_DIR/remote-run-id.stderr"; then + RUN_ID="$(<"$STEP_TMP_DIR/remote-run-id.stdout")" + set_output run_id "$RUN_ID" + else + debug_log "Failed to get remote run-id" + debug_file "$STEP_TMP_DIR/remote-run-id.stderr" + fi + fi + + if [[ $REFRESH_EXIT -eq 0 ]]; then + echo "Refresh complete" + else + if lock-info "$STEP_TMP_DIR/terraform_refresh.stderr"; then + set_output failure-reason state-locked + else + set_output failure-reason refresh-failed + fi + exit 1 + fi +} + +refresh diff --git a/image/src/github_actions/inputs.py b/image/src/github_actions/inputs.py index 1ebaee16..4f5d98e5 100644 --- a/image/src/github_actions/inputs.py +++ b/image/src/github_actions/inputs.py @@ -29,6 +29,7 @@ class PlanPrInputs(PlanInputs): INPUT_TARGET: str INPUT_REPLACE: str INPUT_DESTROY: str + INPUT_REFRESH: str class Plan(PlanPrInputs): diff --git a/image/src/github_pr_comment/__main__.py b/image/src/github_pr_comment/__main__.py index 10648a0e..99cbe13e 100644 --- a/image/src/github_pr_comment/__main__.py +++ b/image/src/github_pr_comment/__main__.py @@ -126,6 +126,9 @@ def format_description(action_inputs: PlanPrInputs, sensitive_variables: List[st if action_inputs["INPUT_DESTROY"] == 'true': mode = '\n:bomb: Planning to destroy all resources' + if action_inputs["INPUT_REFRESH"] == 'false': + mode = '\n:recycle: Skipping refresh of resource state' + if action_inputs['INPUT_LABEL']: return f'{ToolProductName} plan for __{action_inputs["INPUT_LABEL"]}__' + mode diff --git a/terraform-apply/README.md b/terraform-apply/README.md index 4684a2e4..c60cb195 100644 --- a/terraform-apply/README.md +++ b/terraform-apply/README.md @@ -158,6 +158,16 @@ These input values must be the same as any [`dflook/terraform-plan`](https://git - Optional - Default: `false` +* `refresh` + + Set to `false` to skip synchronisation of the Terraform state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + + - Type: boolean + - Optional + - Default: `true` + * `plan_path` Path to a plan file to apply. This would have been generated by a previous [`dflook/terraform-plan`](https://github.com/dflook/terraform-github-actions/tree/main/terraform-plan) action. diff --git a/terraform-apply/action.yaml b/terraform-apply/action.yaml index 1222edff..d8f50dfd 100644 --- a/terraform-apply/action.yaml +++ b/terraform-apply/action.yaml @@ -68,6 +68,13 @@ inputs: This generates and applies a plan in [destroy mode](https://developer.hashicorp.com/terraform/cli/commands/plan#planning-modes). required: false default: "false" + refresh: + description: | + Set to `false` to skip synchronisation of the Terraform state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + required: false + default: "true" plan_path: description: | Path to a plan file to apply. This would have been generated by a previous [`dflook/terraform-plan`](https://github.com/dflook/terraform-github-actions/tree/main/terraform-plan) action. diff --git a/terraform-plan/README.md b/terraform-plan/README.md index e2015eef..a342fef0 100644 --- a/terraform-plan/README.md +++ b/terraform-plan/README.md @@ -116,7 +116,7 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ * `target` - List of resources to apply, one per line. + List of resources to target, one per line. The plan will be limited to these resources and their dependencies. ```yaml @@ -139,6 +139,16 @@ The [dflook/terraform-apply](https://github.com/dflook/terraform-github-actions/ - Optional - Default: `false` +* `refresh` + + Set to `false` to skip synchronisation of the Terraform state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + + - Type: boolean + - Optional + - Default: `true` + * `add_github_comment` Controls whether a comment is added to the PR with the generated plan. diff --git a/terraform-plan/action.yaml b/terraform-plan/action.yaml index 70ffc605..13e8be29 100644 --- a/terraform-plan/action.yaml +++ b/terraform-plan/action.yaml @@ -57,7 +57,7 @@ inputs: default: "" target: description: | - List of resources to apply, one per line. + List of resources to target, one per line. The plan will be limited to these resources and their dependencies. required: false default: "" @@ -68,6 +68,13 @@ inputs: This generates a plan in [destroy mode](https://developer.hashicorp.com/terraform/cli/commands/plan#planning-modes). required: false default: "false" + refresh: + description: | + Set to `false` to skip synchronisation of the Terraform state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + required: false + default: "true" add_github_comment: description: | Controls whether a comment is added to the PR with the generated plan. diff --git a/terraform-refresh/README.md b/terraform-refresh/README.md new file mode 100644 index 00000000..d7f128be --- /dev/null +++ b/terraform-refresh/README.md @@ -0,0 +1,246 @@ +# terraform-refresh action + +This is one of a suite of Terraform related actions - find them at [dflook/terraform-github-actions](https://github.com/dflook/terraform-github-actions). + +This actions runs a Terraform apply operation in refresh-only mode. +This will synchronise the Terraform state with the actual resources, but will not make any changes to the resources. + +## Inputs + +* `path` + + Path to the Terraform root module to refresh state for + + - Type: string + - Optional + - Default: The action workspace + +* `workspace` + + Terraform workspace to run the refresh state in + + - Type: string + - Optional + - Default: `default` + +* `variables` + + Variables to set for the terraform plan. This should be valid Terraform syntax - like a [variable definition file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + +* `backend_config` + + List of Terraform backend config values, one per line. + + ```yaml + with: + backend_config: token=${{ secrets.BACKEND_TOKEN }} + ``` + + - Type: string + - Optional + +* `backend_config_file` + + List of Terraform backend config files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + backend_config_file: prod.backend.tfvars + ``` + + - Type: string + - Optional + +* `target` + + List of resources to target, one per line. + The refresh will be limited to these resources and their dependencies. + + ```yaml + with: + target: | + kubernetes_secret.tls_cert_public + kubernetes_secret.tls_cert_private + ``` + + - Type: string + - Optional + +* `parallelism` + + Limit the number of concurrent operations + + - Type: number + - Optional + - Default: The Terraform default (10). + +## Outputs + +* `failure-reason` + + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The Terraform apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. + + - Type: string + +* `lock-info` + + When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` + + - Type: string + +* `run_id` + + If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + + - Type: string + +## Environment Variables + +* `GITHUB_DOT_COM_TOKEN` + + This is used to specify a token for GitHub.com when the action is running on a GitHub Enterprise instance. + This is only used for downloading OpenTofu binaries from GitHub.com. + If this is not set, an unauthenticated request will be made to GitHub.com to download the binary, which may be rate limited. + + - Type: string + - Optional + +* `TERRAFORM_CLOUD_TOKENS` + + API tokens for cloud hosts, of the form `=`. Multiple tokens may be specified, one per line. + These tokens may be used with the `remote` backend and for fetching required modules from the registry. + + e.g: + + ```yaml + env: + TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_CLOUD_TOKEN }} + ``` + + With other registries: + + ```yaml + env: + TERRAFORM_CLOUD_TOKENS: | + app.terraform.io=${{ secrets.TF_CLOUD_TOKEN }} + terraform.example.com=${{ secrets.TF_REGISTRY_TOKEN }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_SSH_KEY` + + A SSH private key that Terraform will use to fetch git/mercurial module sources. + + This should be in PEM format. + + For example: + + ```yaml + env: + TERRAFORM_SSH_KEY: ${{ secrets.TERRAFORM_SSH_KEY }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_HTTP_CREDENTIALS` + + Credentials that will be used for fetching modules sources with `git::http://`, `git::https://`, `http://` & `https://` schemes. + + Credentials have the format `=:`. Multiple credentials may be specified, one per line. + + Each credential is evaluated in order, and the first matching credentials are used. + + Credentials that are used by git (`git::http://`, `git::https://`) allow a path after the hostname. + Paths are ignored by `http://` & `https://` schemes. + For git module sources, a credential matches if each mentioned path segment is an exact match. + + For example: + + ```yaml + env: + TERRAFORM_HTTP_CREDENTIALS: | + example.com=dflook:${{ secrets.HTTPS_PASSWORD }} + github.com/dflook/terraform-github-actions.git=dflook-actions:${{ secrets.ACTIONS_PAT }} + github.com/dflook=dflook:${{ secrets.DFLOOK_PAT }} + github.com=graham:${{ secrets.GITHUB_PAT }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_PRE_RUN` + + A set of commands that will be ran prior to `terraform init`. This can be used to customise the environment before running Terraform. + + The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. + + The runtime image is currently based on `debian:bullseye`, with the command run using `bash -xeo pipefail`. + + For example: + + ```yaml + env: + TERRAFORM_PRE_RUN: | + # Install latest Azure CLI + curl -skL https://aka.ms/InstallAzureCLIDeb | bash + + # Install postgres client + apt-get install -y --no-install-recommends postgresql-client + ``` + + - Type: string + - Optional diff --git a/terraform-refresh/action.yaml b/terraform-refresh/action.yaml new file mode 100644 index 00000000..938730ca --- /dev/null +++ b/terraform-refresh/action.yaml @@ -0,0 +1,83 @@ +name: terraform-refresh +description: Refresh Terraform state +author: Daniel Flook + +inputs: + path: + description: Path to the Terraform root module to refresh state for + required: false + default: "." + workspace: + description: Terraform workspace to run the refresh state in + required: false + default: "default" + variables: + description: | + Variables to set for the terraform plan. This should be valid Terraform syntax - like a [variable definition file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false + backend_config: + description: List of Terraform backend config values, one per line. + required: false + default: "" + backend_config_file: + description: | + List of Terraform backend config files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false + default: "" + target: + description: | + List of resources to target, one per line. + The refresh will be limited to these resources and their dependencies. + required: false + default: "" + parallelism: + description: Limit the number of concurrent operations + required: false + default: "0" + +outputs: + failure-reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The Terraform apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. + lock-info: + description: | + When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` + run_id: + description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + +runs: + using: docker + image: ../image/Dockerfile + entrypoint: /entrypoints/refresh.sh + +branding: + icon: globe + color: purple diff --git a/tests/workflows/test-refresh/main.tf b/tests/workflows/test-refresh/main.tf new file mode 100644 index 00000000..ee2b831c --- /dev/null +++ b/tests/workflows/test-refresh/main.tf @@ -0,0 +1,14 @@ +resource "local_file" "one" { + filename = "test" + content = "this is my first file" +} + +resource "local_file" "two" { + filename = "test2" + content = "this is my second file" +} + +resource "local_file" "three" { + filename = "test3" + content = "this is my third file" +} diff --git a/tests/workflows/test-refresh/test b/tests/workflows/test-refresh/test new file mode 100755 index 00000000..eee2f131 --- /dev/null +++ b/tests/workflows/test-refresh/test @@ -0,0 +1 @@ +this is my first file \ No newline at end of file diff --git a/tests/workflows/test-refresh/test2 b/tests/workflows/test-refresh/test2 new file mode 100755 index 00000000..5e6f93d6 --- /dev/null +++ b/tests/workflows/test-refresh/test2 @@ -0,0 +1 @@ +this is my second file \ No newline at end of file diff --git a/tests/workflows/test-refresh/test3 b/tests/workflows/test-refresh/test3 new file mode 100755 index 00000000..b14c7504 --- /dev/null +++ b/tests/workflows/test-refresh/test3 @@ -0,0 +1 @@ +this is my third file \ No newline at end of file diff --git a/tofu-apply/README.md b/tofu-apply/README.md index 35196765..42e464c9 100644 --- a/tofu-apply/README.md +++ b/tofu-apply/README.md @@ -158,6 +158,16 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c - Optional - Default: `false` +* `refresh` + + Set to `false` to skip synchronisation of the OpenTofu state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + + - Type: boolean + - Optional + - Default: `true` + * `plan_path` Path to a plan file to apply. This would have been generated by a previous [`dflook/tofu-plan`](https://github.com/dflook/terraform-github-actions/tree/main/tofu-plan) action. diff --git a/tofu-apply/action.yaml b/tofu-apply/action.yaml index ac4647b7..c24d2dae 100644 --- a/tofu-apply/action.yaml +++ b/tofu-apply/action.yaml @@ -68,6 +68,13 @@ inputs: This generates and applies a plan in [destroy mode](https://opentofu.org/docs/cli/commands/plan/#planning-modes). required: false default: "false" + refresh: + description: | + Set to `false` to skip synchronisation of the OpenTofu state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + required: false + default: "true" plan_path: description: | Path to a plan file to apply. This would have been generated by a previous [`dflook/tofu-plan`](https://github.com/dflook/terraform-github-actions/tree/main/tofu-plan) action. diff --git a/tofu-plan/README.md b/tofu-plan/README.md index b63f29a9..12dd078f 100644 --- a/tofu-plan/README.md +++ b/tofu-plan/README.md @@ -116,7 +116,7 @@ The [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ * `target` - List of resources to apply, one per line. + List of resources to target, one per line. The plan will be limited to these resources and their dependencies. ```yaml @@ -139,6 +139,16 @@ The [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ - Optional - Default: `false` +* `refresh` + + Set to `false` to skip synchronisation of the OpenTofu state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + + - Type: boolean + - Optional + - Default: `true` + * `add_github_comment` Controls whether a comment is added to the PR with the generated plan. diff --git a/tofu-plan/action.yaml b/tofu-plan/action.yaml index f2dab4ee..9848d4a3 100644 --- a/tofu-plan/action.yaml +++ b/tofu-plan/action.yaml @@ -57,7 +57,7 @@ inputs: default: "" target: description: | - List of resources to apply, one per line. + List of resources to target, one per line. The plan will be limited to these resources and their dependencies. required: false default: "" @@ -68,6 +68,13 @@ inputs: This generates a plan in [destroy mode](https://opentofu.org/docs/cli/commands/plan/#planning-modes). required: false default: "false" + refresh: + description: | + Set to `false` to skip synchronisation of the OpenTofu state with actual resources. + + This will make the plan faster but may be out of date with the actual resources, which can lead to incorrect plans. + required: false + default: "true" add_github_comment: description: | Controls whether a comment is added to the PR with the generated plan. diff --git a/tofu-refresh/README.md b/tofu-refresh/README.md new file mode 100644 index 00000000..60bf7344 --- /dev/null +++ b/tofu-refresh/README.md @@ -0,0 +1,246 @@ +# tofu-refresh action + +This is one of a suite of OpenTofu related actions - find them at [dflook/terraform-github-actions](https://github.com/dflook/terraform-github-actions). + +This actions runs an OpenTofu apply operation in refresh-only mode. +This will synchronise the OpenTofu state with the actual resources, but will not make any changes to the resources. + +## Inputs + +* `path` + + Path to the OpenTofu root module to refresh state for + + - Type: string + - Optional + - Default: The action workspace + +* `workspace` + + OpenTofu workspace to run the refresh state in + + - Type: string + - Optional + - Default: `default` + +* `variables` + + Variables to set for the tofu plan. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + +* `backend_config` + + List of OpenTofu backend config values, one per line. + + ```yaml + with: + backend_config: token=${{ secrets.BACKEND_TOKEN }} + ``` + + - Type: string + - Optional + +* `backend_config_file` + + List of OpenTofu backend config files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + backend_config_file: prod.backend.tfvars + ``` + + - Type: string + - Optional + +* `target` + + List of resources to target, one per line. + The refresh will be limited to these resources and their dependencies. + + ```yaml + with: + target: | + kubernetes_secret.tls_cert_public + kubernetes_secret.tls_cert_private + ``` + + - Type: string + - Optional + +* `parallelism` + + Limit the number of concurrent operations + + - Type: number + - Optional + - Default: The OpenTofu default (10). + +## Outputs + +* `failure-reason` + + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The OpenTofu apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. + + - Type: string + +* `lock-info` + + When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` + + - Type: string + +* `run_id` + + If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + + - Type: string + +## Environment Variables + +* `GITHUB_DOT_COM_TOKEN` + + This is used to specify a token for GitHub.com when the action is running on a GitHub Enterprise instance. + This is only used for downloading OpenTofu binaries from GitHub.com. + If this is not set, an unauthenticated request will be made to GitHub.com to download the binary, which may be rate limited. + + - Type: string + - Optional + +* `TERRAFORM_CLOUD_TOKENS` + + API tokens for cloud hosts, of the form `=`. Multiple tokens may be specified, one per line. + These tokens may be used with the `remote` backend and for fetching required modules from the registry. + + e.g: + + ```yaml + env: + TERRAFORM_CLOUD_TOKENS: app.terraform.io=${{ secrets.TF_CLOUD_TOKEN }} + ``` + + With other registries: + + ```yaml + env: + TERRAFORM_CLOUD_TOKENS: | + app.terraform.io=${{ secrets.TF_CLOUD_TOKEN }} + tofu.example.com=${{ secrets.TF_REGISTRY_TOKEN }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_SSH_KEY` + + A SSH private key that OpenTofu will use to fetch git/mercurial module sources. + + This should be in PEM format. + + For example: + + ```yaml + env: + TERRAFORM_SSH_KEY: ${{ secrets.TERRAFORM_SSH_KEY }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_HTTP_CREDENTIALS` + + Credentials that will be used for fetching modules sources with `git::http://`, `git::https://`, `http://` & `https://` schemes. + + Credentials have the format `=:`. Multiple credentials may be specified, one per line. + + Each credential is evaluated in order, and the first matching credentials are used. + + Credentials that are used by git (`git::http://`, `git::https://`) allow a path after the hostname. + Paths are ignored by `http://` & `https://` schemes. + For git module sources, a credential matches if each mentioned path segment is an exact match. + + For example: + + ```yaml + env: + TERRAFORM_HTTP_CREDENTIALS: | + example.com=dflook:${{ secrets.HTTPS_PASSWORD }} + github.com/dflook/terraform-github-actions.git=dflook-actions:${{ secrets.ACTIONS_PAT }} + github.com/dflook=dflook:${{ secrets.DFLOOK_PAT }} + github.com=graham:${{ secrets.GITHUB_PAT }} + ``` + + - Type: string + - Optional + +* `TERRAFORM_PRE_RUN` + + A set of commands that will be ran prior to `tofu init`. This can be used to customise the environment before running OpenTofu. + + The runtime environment for these actions is subject to change in minor version releases. If using this environment variable, specify the minor version of the action to use. + + The runtime image is currently based on `debian:bullseye`, with the command run using `bash -xeo pipefail`. + + For example: + + ```yaml + env: + TERRAFORM_PRE_RUN: | + # Install latest Azure CLI + curl -skL https://aka.ms/InstallAzureCLIDeb | bash + + # Install postgres client + apt-get install -y --no-install-recommends postgresql-client + ``` + + - Type: string + - Optional diff --git a/tofu-refresh/action.yaml b/tofu-refresh/action.yaml new file mode 100644 index 00000000..0e27cd8b --- /dev/null +++ b/tofu-refresh/action.yaml @@ -0,0 +1,85 @@ +name: tofu-refresh +description: Refresh OpenTofu state +author: Daniel Flook + +inputs: + path: + description: Path to the OpenTofu root module to refresh state for + required: false + default: "." + workspace: + description: OpenTofu workspace to run the refresh state in + required: false + default: "default" + variables: + description: | + Variables to set for the tofu plan. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false + backend_config: + description: List of OpenTofu backend config values, one per line. + required: false + default: "" + backend_config_file: + description: | + List of OpenTofu backend config files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false + default: "" + target: + description: | + List of resources to target, one per line. + The refresh will be limited to these resources and their dependencies. + required: false + default: "" + parallelism: + description: Limit the number of concurrent operations + required: false + default: "0" + +outputs: + failure-reason: + description: | + When the job outcome is `failure`, this output may be set. The value may be one of: + + - `refresh-failed` - The OpenTofu apply operation failed. + - `state-locked` - The Terraform state lock could not be obtained because it was already locked. + + If the job fails for any other reason this will not be set. + This can be used with the Actions expression syntax to conditionally run steps. + lock-info: + description: | + When the job outcome is `failure` and the failure-reason is `state-locked`, this output will be set. + + It is a json object containing any available state lock information and typically has the form: + + ```json + { + "ID": "838fbfde-c5cd-297f-84a4-d7578b4a4880", + "Path": "terraform-github-actions/test-unlock-state", + "Operation": "OperationTypeApply", + "Who": "root@e9d43b0c6478", + "Version": "1.3.7", + "Created": "2023-01-28 00:16:41.560904373 +0000 UTC", + "Info": "" + } + ``` + run_id: + description: If the root module uses the `remote` or `cloud` backend in remote execution mode, this output will be set to the remote run id. + +runs: + env: + OPENTOFU: true + using: docker + image: ../image/Dockerfile + entrypoint: /entrypoints/refresh.sh + +branding: + icon: globe + color: purple