From b58301acdd252db1784431604648cefc2f779775 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 9 Jun 2025 18:40:48 +0100 Subject: [PATCH 1/2] Add OpenTofu 1.9 exclude parameter support --- .github/actionlint.yaml | 5 +- ....yaml => test-target-replace-exclude.yaml} | 126 ++++++++++++++---- .gitignore | 3 + docs-gen/action.py | 1 + docs-gen/actions/apply.py | 5 + docs-gen/actions/plan.py | 2 + docs-gen/actions/refresh.py | 5 + docs-gen/inputs/exclude.py | 23 ++++ image/actions.sh | 14 ++ image/entrypoints/refresh.sh | 8 ++ image/src/github_actions/inputs.py | 1 + image/src/github_pr_comment/__main__.py | 10 ++ image/src/terraform_version/local_state.py | 5 +- .../test-target-replace-exclude/main.tf | 61 +++++++++ tests/workflows/test-target-replace/main.tf | 29 ---- tofu-apply/README.md | 15 +++ tofu-apply/action.yaml | 6 + tofu-plan/README.md | 17 +++ tofu-plan/action.yaml | 8 ++ tofu-refresh/README.md | 15 +++ tofu-refresh/action.yaml | 6 + 21 files changed, 310 insertions(+), 55 deletions(-) rename .github/workflows/{test-target-replace.yaml => test-target-replace-exclude.yaml} (80%) create mode 100644 docs-gen/inputs/exclude.py create mode 100644 tests/workflows/test-target-replace-exclude/main.tf delete mode 100644 tests/workflows/test-target-replace/main.tf diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 1bafb250..4abdd4bb 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -30,10 +30,13 @@ paths: - 'property "git_https" is not defined in object type' - 'property "awkward_.*" is not defined in object type' - 'property "word" is not defined in object type' - .github/workflows/test-target-replace.yaml: + .github/workflows/test-target-replace-exclude.yaml: ignore: - 'property "count" is not defined in object type' - 'property "foreach" is not defined in object type' + - 'property "keep_me" is not defined in object type' + - 'property "exclude_me" is not defined in object type' + - 'property "also_exclude" is not defined in object type' .github/workflows/release.yaml: ignore: - 'Useless cat.' diff --git a/.github/workflows/test-target-replace.yaml b/.github/workflows/test-target-replace-exclude.yaml similarity index 80% rename from .github/workflows/test-target-replace.yaml rename to .github/workflows/test-target-replace-exclude.yaml index 3753ed83..ff3f313f 100644 --- a/.github/workflows/test-target-replace.yaml +++ b/.github/workflows/test-target-replace-exclude.yaml @@ -1,4 +1,4 @@ -name: Test actions using target and replace +name: Test actions using target, replace, and exclude on: - pull_request @@ -27,7 +27,7 @@ jobs: id: plan with: label: test-target-replace plan_targeting - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.notpresent variables: | @@ -46,7 +46,7 @@ jobs: uses: ./terraform-plan id: plan-first-change with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.count[0] variables: | @@ -65,7 +65,7 @@ jobs: uses: ./terraform-apply id: apply-first-change with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.count[0] variables: | @@ -84,7 +84,7 @@ jobs: uses: ./terraform-plan id: plan-second-change with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.foreach["hello"] variables: | @@ -103,7 +103,7 @@ jobs: uses: ./terraform-apply id: apply-second-change with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.foreach["hello"] variables: | @@ -129,7 +129,7 @@ jobs: uses: ./terraform-apply id: apply-third-change with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.count[0] random_string.foreach["hello"] @@ -158,7 +158,7 @@ jobs: uses: ./terraform-plan id: plan-targeted-replacement with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.foreach["hello"] replace: | @@ -180,7 +180,7 @@ jobs: uses: ./terraform-apply id: apply-targeted-replacement with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude target: | random_string.foreach["hello"] replace: | @@ -210,7 +210,7 @@ jobs: uses: ./terraform-plan id: plan-replacement with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude replace: | random_string.foreach["hello"] random_string.count[0] @@ -230,7 +230,7 @@ jobs: uses: ./terraform-apply id: apply-replacement with: - path: tests/workflows/test-target-replace + path: tests/workflows/test-target-replace-exclude replace: | random_string.foreach["hello"] random_string.count[0] @@ -268,7 +268,7 @@ jobs: - name: Setup remote backend run: | - cat >tests/workflows/test-target-replace/backend.tf <tests/workflows/test-target-replace-exclude/backend.tf < TerraformComment: if target := os.environ.get('INPUT_TARGET'): plan_modifier['target'] = sorted(t.strip() for t in target.replace(',', '\n', ).split('\n') if t.strip()) + if exclude := os.environ.get('INPUT_EXCLUDE'): + plan_modifier['exclude'] = sorted(t.strip() for t in exclude.replace(',', '\n', ).split('\n') if t.strip()) + if replace := os.environ.get('INPUT_REPLACE'): plan_modifier['replace'] = sorted(t.strip() for t in replace.replace(',', '\n', ).split('\n') if t.strip()) @@ -350,6 +357,9 @@ def get_comment(action_inputs: PlanPrInputs, backend_fingerprint: bytes, backup_ if target := os.environ.get('INPUT_TARGET'): plan_modifier['target'] = sorted(t.strip() for t in target.replace(',', '\n', ).split('\n') if t.strip()) + if exclude := os.environ.get('INPUT_EXCLUDE'): + plan_modifier['exclude'] = sorted(t.strip() for t in exclude.replace(',', '\n', ).split('\n') if t.strip()) + if replace := os.environ.get('INPUT_REPLACE'): plan_modifier['replace'] = sorted(t.strip() for t in replace.replace(',', '\n', ).split('\n') if t.strip()) diff --git a/image/src/terraform_version/local_state.py b/image/src/terraform_version/local_state.py index 33e8277f..294aa05d 100644 --- a/image/src/terraform_version/local_state.py +++ b/image/src/terraform_version/local_state.py @@ -19,7 +19,10 @@ def read_local_state(module_dir: Path) -> Optional[Version]: with open(state_path) as f: state = json.load(f) if state.get('serial') > 0: - return Version(state.get('terraform_version')) + # Respect OPENTOFU environment variable when determining product type + # since OpenTofu maintains compatibility with Terraform state files + product = 'OpenTofu' if 'OPENTOFU' in os.environ else 'Terraform' + return Version(state.get('terraform_version'), product) except Exception as e: debug(str(e)) diff --git a/tests/workflows/test-target-replace-exclude/main.tf b/tests/workflows/test-target-replace-exclude/main.tf new file mode 100644 index 00000000..e5d8d771 --- /dev/null +++ b/tests/workflows/test-target-replace-exclude/main.tf @@ -0,0 +1,61 @@ +resource "random_string" "count" { + count = 1 + + length = var.length + + special = false + min_special = 0 +} + +resource "random_string" "foreach" { + for_each = toset(["hello"]) + + length = var.length + + special = false + min_special = 0 +} + +# Additional resources for exclude testing +resource "random_string" "exclude_me" { + length = var.length + special = false + min_special = 0 +} + +resource "random_string" "keep_me" { + length = var.length + special = false + min_special = 0 +} + +resource "random_string" "also_exclude" { + length = var.length + special = false + min_special = 0 +} + + +variable "length" { + +} + +output "count" { + value = random_string.count[0].result +} + +output "foreach" { + value = random_string.foreach["hello"].result +} + +output "exclude_me" { + value = random_string.exclude_me.result +} + +output "keep_me" { + value = random_string.keep_me.result +} + +output "also_exclude" { + value = random_string.also_exclude.result +} diff --git a/tests/workflows/test-target-replace/main.tf b/tests/workflows/test-target-replace/main.tf deleted file mode 100644 index 16fd8bd3..00000000 --- a/tests/workflows/test-target-replace/main.tf +++ /dev/null @@ -1,29 +0,0 @@ -resource "random_string" "count" { - count = 1 - - length = var.length - - special = false - min_special = 0 -} - -resource "random_string" "foreach" { - for_each = toset(["hello"]) - - length = var.length - - special = false - min_special = 0 -} - -variable "length" { - -} - -output "count" { - value = random_string.count[0].result -} - -output "foreach" { - value = random_string.foreach["hello"].result -} diff --git a/tofu-apply/README.md b/tofu-apply/README.md index 7a5bda6b..935e09b5 100644 --- a/tofu-apply/README.md +++ b/tofu-apply/README.md @@ -148,6 +148,21 @@ These input values must be the same as any [`dflook/tofu-plan`](https://github.c - Type: string - Optional +* `exclude` + + List of resources to exclude from the apply operation, one per line. + The apply operation will include all resources except the specified ones and their dependencies. + + ```yaml + with: + exclude: | + local_file.sensitive_config + aws_instance.temp_resource + ``` + + - Type: string + - Optional + * `destroy` Set to `true` to destroy all resources. diff --git a/tofu-apply/action.yaml b/tofu-apply/action.yaml index 8de2df12..7973f95b 100644 --- a/tofu-apply/action.yaml +++ b/tofu-apply/action.yaml @@ -50,6 +50,12 @@ inputs: The apply operation will be limited to these resources and their dependencies. required: false default: "" + exclude: + description: | + List of resources to exclude from the apply operation, one per line. + The apply operation will include all resources except the specified ones and their dependencies. + required: false + default: "" destroy: description: | Set to `true` to destroy all resources. diff --git a/tofu-plan/README.md b/tofu-plan/README.md index a5f264a4..787ed381 100644 --- a/tofu-plan/README.md +++ b/tofu-plan/README.md @@ -129,6 +129,23 @@ The [dflook/tofu-apply](https://github.com/dflook/terraform-github-actions/tree/ - Type: string - Optional +* `exclude` + + List of resources to exclude from operations, one per line. + The plan will include all resources except the specified ones and their dependencies. + + Requires OpenTofu 1.9+. + + ```yaml + with: + exclude: | + local_file.sensitive_config + aws_instance.temp_resource + ``` + + - Type: string + - Optional + * `destroy` Set to `true` to generate a plan to destroy all resources. diff --git a/tofu-plan/action.yaml b/tofu-plan/action.yaml index 1aa8cf95..6b8b67a2 100644 --- a/tofu-plan/action.yaml +++ b/tofu-plan/action.yaml @@ -50,6 +50,14 @@ inputs: The plan will be limited to these resources and their dependencies. required: false default: "" + exclude: + description: | + List of resources to exclude from operations, one per line. + The plan will include all resources except the specified ones and their dependencies. + + Requires OpenTofu 1.9+. + required: false + default: "" destroy: description: | Set to `true` to generate a plan to destroy all resources. diff --git a/tofu-refresh/README.md b/tofu-refresh/README.md index c43d6b0e..07e39ea9 100644 --- a/tofu-refresh/README.md +++ b/tofu-refresh/README.md @@ -97,6 +97,21 @@ This will synchronise the OpenTofu state with the actual resources, but will not - Type: string - Optional +* `exclude` + + List of resources to exclude from the refresh operation, one per line. + The refresh will include all resources except the specified ones and their dependencies. + + ```yaml + with: + exclude: | + local_file.sensitive_config + aws_instance.temp_resource + ``` + + - Type: string + - Optional + * `parallelism` Limit the number of concurrent operations diff --git a/tofu-refresh/action.yaml b/tofu-refresh/action.yaml index 0e27cd8b..ad03455c 100644 --- a/tofu-refresh/action.yaml +++ b/tofu-refresh/action.yaml @@ -38,6 +38,12 @@ inputs: The refresh will be limited to these resources and their dependencies. required: false default: "" + exclude: + description: | + List of resources to exclude from the refresh operation, one per line. + The refresh will include all resources except the specified ones and their dependencies. + required: false + default: "" parallelism: description: Limit the number of concurrent operations required: false From 0d312e091e7107527e78be3db8ba653c173ab834 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Mon, 9 Jun 2025 21:33:09 +0100 Subject: [PATCH 2/2] Remove blank line --- .../test-target-replace-exclude.yaml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/test-target-replace-exclude.yaml b/.github/workflows/test-target-replace-exclude.yaml index ff3f313f..048ef2bf 100644 --- a/.github/workflows/test-target-replace-exclude.yaml +++ b/.github/workflows/test-target-replace-exclude.yaml @@ -626,3 +626,48 @@ jobs: exit 1 fi + - name: Plan excluding resources without label + uses: ./tofu-plan + id: plan-exclude-no-label + with: + path: tests/workflows/test-target-replace-exclude + exclude: | + random_string.exclude_me + variables: | + length = 8 + + - name: Verify exclude plan without label + env: + CHANGES: ${{ steps.plan-exclude-no-label.outputs.changes }} + run: | + if [[ "$CHANGES" != "true" ]]; then + echo "::error:: Exclude plan without label should have changes for non-excluded resources" + exit 1 + fi + + - name: Apply excluding resources without label + uses: ./tofu-apply + id: apply-exclude-no-label + with: + path: tests/workflows/test-target-replace-exclude + exclude: | + random_string.exclude_me + variables: | + length = 8 + + - name: Verify exclude apply without label + env: + EXCLUDE_ME: ${{ steps.apply-exclude-no-label.outputs.exclude_me }} + KEEP_ME: ${{ steps.apply-exclude-no-label.outputs.keep_me }} + run: | + # Should NOT have created excluded resource + if [[ "$EXCLUDE_ME" != "" ]]; then + echo "::error:: exclude_me output should be empty (resource excluded without label)" + exit 1 + fi + + # Should have updated non-excluded resource + if [[ "$KEEP_ME" == "" ]]; then + echo "::error:: keep_me output should be set (resource not excluded without label)" + exit 1 + fi