diff --git a/.changes/1.13.0-alpha.1.md b/.changes/1.13.0-alpha.1.md new file mode 100644 index 000000000..52cbf0b06 --- /dev/null +++ b/.changes/1.13.0-alpha.1.md @@ -0,0 +1,6 @@ +## 1.13.0-alpha.1 (March 27, 2025) + +NOTES: + +* This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `terraform-plugin-framework@v1.15.0-alpha.1` or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) + diff --git a/.changes/1.13.0-beta.1.md b/.changes/1.13.0-beta.1.md new file mode 100644 index 000000000..29e617ea3 --- /dev/null +++ b/.changes/1.13.0-beta.1.md @@ -0,0 +1,15 @@ +## 1.13.0-beta.1 (April 18, 2025) + +BREAKING CHANGES: + +* importstate: `ImportStatePersist` and `ImportStateVerify` are not supported for plannable import (`ImportBlockWith*`) and will return an error ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) +* importstate: renamed `ImportStateWithId` to `ImportStateWithID` and renamed `ImportCommandWithId` to `ImportCommandWithID`. ([#465](https://github.com/hashicorp/terraform-plugin-testing/issues/465)) + +NOTES: + +* This beta pre-release adds support for managed resource identity, which can be used with Terraform v1.12.0-beta2. Acceptance tests can use the `ImportBlockWithResourceIdentity` kind to exercise the import of a managed resource using its resource identity object values instead of using a string identifier. ([#480](https://github.com/hashicorp/terraform-plugin-testing/issues/480)) + +BUG FIXES: + +* importstate: plannable import (`ImportBlockWith*`) fixed for a resource with a dependency ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) + diff --git a/.changes/1.13.0.md b/.changes/1.13.0.md new file mode 100644 index 000000000..b056b9e2b --- /dev/null +++ b/.changes/1.13.0.md @@ -0,0 +1,20 @@ +## 1.13.0 (May 16, 2025) + +NOTES: + +* reduced the volume of DEBUG-level logging to make it easier to visually scan debug output ([#463](https://github.com/hashicorp/terraform-plugin-testing/issues/463)) + +FEATURES: + +* ImportState: Added support for testing plannable import via Terraform configuration. Configuration is used from the previous test step if available. `Config`, `ConfigFile`, and `ConfigDirectory` can also be used directly with `ImportState` if needed. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateKind` to control which method of import the `ImportState` test step uses. `ImportCommandWithID` (default, same behavior as today) , `ImportBlockWithID`, and `ImportBlockWithResourceIdentity`. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateConfigExact` to opt-out of new import config generation for plannable import. ([#494](https://github.com/hashicorp/terraform-plugin-testing/issues/494)) +* statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) +* statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) + +ENHANCEMENTS: + +* statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state. ([#468](https://github.com/hashicorp/terraform-plugin-testing/issues/468)) +* statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) +* Adds `AdditionalCLIOptions.PlanOptions.NoRefresh` to test `terraform plan -refresh=false` ([#490](https://github.com/hashicorp/terraform-plugin-testing/issues/490)) + diff --git a/.copywrite.hcl b/.copywrite.hcl index 301109050..b9f35eddf 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -5,8 +5,11 @@ project { copyright_year = 2014 header_ignore = [ + # internal catalog metadata (prose) + "META.d/**/*.yaml", + # changie tooling configuration and CHANGELOG entries (prose) - ".changes/unreleased/*.yaml", + ".changes/unreleased/**", ".changie.yaml", # GitHub issue template configuration diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d51ee4eac..ed6d5d312 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,17 +1,17 @@ -* @hashicorp/terraform-devex +* @hashicorp/terraform-core-plugins # engineering and web presence get notified of, and can approve changes to web tooling, but not content. -/website/ @hashicorp/web-presence @hashicorp/terraform-devex +/website/ @hashicorp/web-presence @hashicorp/terraform-core-plugins /website/data/ /website/public/ /website/content/ # education and engineering get notified of, and can approve changes to web content. -/website/data/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -/website/public/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -/website/content/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -/website/docs/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -/website/img/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex -/website/README.md @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-devex \ No newline at end of file +/website/data/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/public/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/content/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/docs/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/img/ @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins +/website/README.md @hashicorp/team-docs-packer-and-terraform @hashicorp/terraform-core-plugins \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 700329227..66573fb25 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -18,11 +18,24 @@ while we're on v0, breaking changes will be accepted during minor releases. - [I just have a question](#i-just-have-a-question) - [I want to report a vulnerability](#i-want-to-report-a-vulnerability) - [New Issue](#new-issue) + * [Bug Reports](#bug-reports) + * [Feature Requests](#feature-requests) + * [Documentation Contributions](#documentation-contributions) - [New Pull Request](#new-pull-request) + * [Cosmetic changes, code formatting, and typos](#cosmetic-changes-code-formatting-and-typos) + + [Exceptions](#exceptions) + * [License Headers](#license-headers) + - [Linting](#linting) + - [Testing](#testing) + * [GitHub Actions Tests](#github-actions-tests) + * [Go Unit Tests](#go-unit-tests) + - [Maintainers Guide](#maintainers-guide) + * [Releases](#releases) ## I just have a question -> **Note:** We use GitHub for tracking bugs and feature requests related to +> [!Note] +> We use GitHub for tracking bugs and feature requests related to > terraform-plugin-testing. For questions, please see relevant channels at @@ -31,7 +44,7 @@ https://www.terraform.io/community.html ## I want to report a vulnerability Please disclose security vulnerabilities responsibly by following the procedure -described at https://www.hashicorp.com/security#vulnerability-reporting +described at https://www.hashicorp.com/en/trust/security/vulnerability-management ## New Issue diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7d1ea9865..96eaeaad9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,10 +11,7 @@ updates: directory: "/tools" schedule: interval: "daily" - # Dependabot only updates hashicorp GHAs, external GHAs are managed by internal tooling (tsccr) - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" - allow: - - dependency-name: "hashicorp/*" diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 8451dde35..ebdf8a6b8 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 61a7e6260..30e29a976 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -17,11 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 + - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 test: name: test (Go ${{ matrix.go-version }} / TF ${{ matrix.terraform }}) runs-on: ubuntu-latest @@ -31,7 +31,7 @@ jobs: terraform: ${{ fromJSON(vars.TF_VERSIONS_PROTOCOL_V5) }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go-version }} - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 @@ -49,7 +49,7 @@ jobs: wildcard=".*" echo "version=${orginal_version%"$wildcard"}" >> "$GITHUB_OUTPUT" - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: go-${{ matrix.go-version }}-terraform-${{ steps.tf_version.outputs.version }}-coverage path: coverage.html diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 76735ffb5..8750d649e 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -15,9 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb7bc6abf..48cd9c3d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' @@ -93,7 +93,7 @@ jobs: cd .changes sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.golangci.yml b/.golangci.yml index 8b0f17d94..7cde8b21e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,16 +1,12 @@ -issues: - max-issues-per-linter: 0 - max-same-issues: 0 - +version: "2" linters: - disable-all: true + default: none enable: - copyloopvar - durationcheck - errcheck - forcetypeassert - - gofmt - - gosimple + - govet - ineffassign - makezero - misspell @@ -22,8 +18,37 @@ linters: - unparam - unused - usetesting - - govet - -run: - # Prevent false positive timeouts in CI - timeout: 5m + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + settings: + staticcheck: + checks: + - all + - '-QF1001' # "could apply De Morgan's law" -- https://staticcheck.dev/docs/checks/#QF1001 + - '-QF1002' # "could use tagged switch" -- https://staticcheck.dev/docs/checks/#QF1002 + - '-QF1004' # "could use strings.ReplaceAll instead" -- https://staticcheck.dev/docs/checks/#QF1004 + - '-QF1008' # "could remove embedded field "Block" from selector" -- https://staticcheck.dev/docs/checks/#QF1008 + - '-ST1003' # example: "const autoTFVarsJson should be autoTFVarsJSON" -- https://staticcheck.dev/docs/checks/#ST1003 + - '-ST1005' # "error strings should not end with punctuation or newlines" -- https://staticcheck.dev/docs/checks/#ST1005 + - '-ST1016' # example: "methods on the same type should have the same receiver name (seen 2x "r", 2x "s")" -- https://staticcheck.dev/docs/checks/#ST1016 +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofmt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yml b/.goreleaser.yml index 91d69e904..74f85eb59 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -5,5 +5,6 @@ builds: milestones: - close: true release: + prerelease: auto ids: - 'none' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af99a795..3085ae21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## 1.13.0 (May 16, 2025) + +NOTES: + +* reduced the volume of DEBUG-level logging to make it easier to visually scan debug output ([#463](https://github.com/hashicorp/terraform-plugin-testing/issues/463)) + +FEATURES: + +* ImportState: Added support for testing plannable import via Terraform configuration. Configuration is used from the previous test step if available. `Config`, `ConfigFile`, and `ConfigDirectory` can also be used directly with `ImportState` if needed. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateKind` to control which method of import the `ImportState` test step uses. `ImportCommandWithID` (default, same behavior as today) , `ImportBlockWithID`, and `ImportBlockWithResourceIdentity`. ([#442](https://github.com/hashicorp/terraform-plugin-testing/issues/442)) +* ImportState: Added `ImportStateConfigExact` to opt-out of new import config generation for plannable import. ([#494](https://github.com/hashicorp/terraform-plugin-testing/issues/494)) +* statecheck: Added `ExpectIdentityValueMatchesState` state check to assert that an identity value matches a state value at the same path. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) +* statecheck: Added `ExpectIdentityValueMatchesStateAtPath` state check to assert that an identity value matches a state value at different paths. ([#503](https://github.com/hashicorp/terraform-plugin-testing/issues/503)) + +ENHANCEMENTS: + +* statecheck: Added `ExpectIdentityValue` state check, which asserts a specified attribute value of a managed resource identity in state. ([#468](https://github.com/hashicorp/terraform-plugin-testing/issues/468)) +* statecheck: Added `ExpectIdentity` state check, which asserts all data of a managed resource identity in state. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) +* Adds `AdditionalCLIOptions.PlanOptions.NoRefresh` to test `terraform plan -refresh=false` ([#490](https://github.com/hashicorp/terraform-plugin-testing/issues/490)) + +## 1.13.0-beta.1 (April 18, 2025) + +BREAKING CHANGES: + +* importstate: `ImportStatePersist` and `ImportStateVerify` are not supported for plannable import (`ImportBlockWith*`) and will return an error ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) +* importstate: renamed `ImportStateWithId` to `ImportStateWithID` and renamed `ImportCommandWithId` to `ImportCommandWithID`. ([#465](https://github.com/hashicorp/terraform-plugin-testing/issues/465)) + +NOTES: + +* This beta pre-release adds support for managed resource identity, which can be used with Terraform v1.12.0-beta2. Acceptance tests can use the `ImportBlockWithResourceIdentity` kind to exercise the import of a managed resource using its resource identity object values instead of using a string identifier. ([#480](https://github.com/hashicorp/terraform-plugin-testing/issues/480)) + +BUG FIXES: + +* importstate: plannable import (`ImportBlockWith*`) fixed for a resource with a dependency ([#476](https://github.com/hashicorp/terraform-plugin-testing/issues/476)) + +## 1.13.0-alpha.1 (March 27, 2025) + +NOTES: + +* This alpha pre-release contains testing utilities for managed resource identity, which can be used with `Terraform v1.12.0-alpha20250319`, to assert identity data stored during apply workflows. A managed resource in a provider can read/store identity data using the `terraform-plugin-framework@v1.15.0-alpha.1` or `terraform-plugin-sdk/v2@v2.37.0-alpha.1` Go modules. To assert identity data stored by a provider in state, use the `statecheck.ExpectIdentity` state check. ([#470](https://github.com/hashicorp/terraform-plugin-testing/issues/470)) + ## 1.12.0 (March 18, 2025) NOTES: diff --git a/META.d/_summary.yaml b/META.d/_summary.yaml new file mode 100644 index 000000000..56ab752a7 --- /dev/null +++ b/META.d/_summary.yaml @@ -0,0 +1,10 @@ +--- +schema: 1.1 + +partition: tf-ecosystem + +summary: + owner: team-tf-core-plugins + description: | + Module for testing Terraform providers + visibility: public diff --git a/README.md b/README.md index f14d5e49b..05e1b5344 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ This project follows the [support policy](https://golang.org/doc/devel/release.h Currently, that means Go **1.23** or later must be used when including this project as a dependency. +## Documentation + +Visit the [Testing Terraform Plugins docs](https://developer.hashicorp.com/terraform/plugin/testing) to learn about how to best use this helper module. + ## Contributing See [`.github/CONTRIBUTING.md`](https://github.com/hashicorp/terraform-plugin-testing/blob/main/.github/CONTRIBUTING.md) diff --git a/go.mod b/go.mod index 0c532d06f..5dae181dc 100644 --- a/go.mod +++ b/go.mod @@ -10,33 +10,33 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hc-install v0.9.1 + github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.22.0 - github.com/hashicorp/terraform-json v0.24.0 - github.com/hashicorp/terraform-plugin-go v0.26.0 + github.com/hashicorp/terraform-exec v0.23.0 + github.com/hashicorp/terraform-json v0.25.0 + github.com/hashicorp/terraform-plugin-go v0.27.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/mitchellh/go-testing-interface v1.14.1 github.com/zclconf/go-cty v1.16.2 - golang.org/x/crypto v0.36.0 + golang.org/x/crypto v0.38.0 ) require ( - github.com/ProtonMail/go-crypto v1.1.3 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-registry-address v0.2.5 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -49,14 +49,14 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index f56d072f5..b94ba8538 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= @@ -11,10 +11,10 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,18 +25,18 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= -github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= -github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= -github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -61,8 +61,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -70,24 +70,24 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= -github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= -github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= -github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= -github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= -github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= +github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= +github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGoxseG1hLhoQ= +github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc= +github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE= +github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4= -github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= -github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= +github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= +github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -124,14 +124,14 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= -github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= @@ -150,34 +150,36 @@ github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70 github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -190,18 +192,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -212,14 +214,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helper/resource/additional_cli_options.go b/helper/resource/additional_cli_options.go index 62578edef..36dc0f89a 100644 --- a/helper/resource/additional_cli_options.go +++ b/helper/resource/additional_cli_options.go @@ -23,4 +23,7 @@ type ApplyOptions struct { type PlanOptions struct { // AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command. AllowDeferral bool + + // NoRefresh will pass the `-refresh=false` flag to the plan command. + NoRefresh bool } diff --git a/helper/resource/importstate/examplecloud_test.go b/helper/resource/importstate/examplecloud_test.go new file mode 100644 index 000000000..bf1e2747d --- /dev/null +++ b/helper/resource/importstate/examplecloud_test.go @@ -0,0 +1,622 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" +) + +func examplecloudDataSource() testprovider.DataSource { + return testprovider.DataSource{ + ReadResponse: &datasource.ReadResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "datasource-test"), + }, + ), + }, + SchemaResponse: &datasource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + }, + }, + }, + }, + } +} + +func examplecloudResource() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + } +} + +// examplecloudZone is a test resource that mimics a DNS zone resource. +func examplecloudZone() testprovider.Resource { + value := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"), + "name": tftypes.NewValue(tftypes.String, "example.net"), + }, + ) + + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: value, + }, + ReadResponse: &resource.ReadResponse{ + NewState: value, + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: value, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + } +} + +// examplecloudZoneRecord is a test resource that mimics a DNS zone record resource. +// It models a resource dependency; specifically, it depends on a DNS zone ID and will +// plan a replacement if the zone ID changes. +func examplecloudZoneRecord() testprovider.Resource { + value := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "zone_id": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"), + "zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"), + "name": tftypes.NewValue(tftypes.String, "www"), + }, + ) + + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: value, + }, + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.RequiresReplace = []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("zone_id"), + } + }, + ReadResponse: &resource.ReadResponse{ + NewState: value, + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: value, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("zone_id"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + } +} + +func examplecloudResourceWithEveryIdentitySchemaType() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast"), + }), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hostname": tftypes.String, + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "hostname": tftypes.NewValue(tftypes.String, "mail.example.net"), + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "cabinet": tftypes.String, + "unit": tftypes.Number, + "active": tftypes.Bool, + "tags": tftypes.List{ElementType: tftypes.String}, + "magic_numbers": tftypes.List{ElementType: tftypes.Number}, + "beep_boop": tftypes.List{ElementType: tftypes.Bool}, + }, + }, + map[string]tftypes.Value{ + "cabinet": tftypes.NewValue(tftypes.String, "A1"), + "unit": tftypes.NewValue(tftypes.Number, 14), + "active": tftypes.NewValue(tftypes.Bool, true), + "tags": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "storage"), + tftypes.NewValue(tftypes.String, "fast")}), + "magic_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 5), + tftypes.NewValue(tftypes.Number, 2)}), + "beep_boop": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Bool}, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("hostname"), + RequiredStringAttribute("cabinet"), + RequiredNumberAttribute("unit"), + RequiredBoolAttribute("active"), + RequiredListAttribute("tags", tftypes.String), + OptionalComputedListAttribute("magic_numbers", tftypes.Number), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "cabinet", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "unit", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "active", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "tags", + Type: tftypes.List{ + ElementType: tftypes.String, + }, + OptionalForImport: true, + }, + { + Name: "magic_numbers", + Type: tftypes.List{ + ElementType: tftypes.Number, + }, + OptionalForImport: true, + }, + }, + }, + }, + } +} + +func examplecloudResourceWithNullIdentityAttr() testprovider.Resource { + return testprovider.Resource{ + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "location": tftypes.String, + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "location": tftypes.NewValue(tftypes.String, "westeurope"), + "name": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + Identity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "value_we_dont_always_need": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "westeurope/somevalue"), + "value_we_dont_always_need": tftypes.NewValue(tftypes.String, nil), + }, + )), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + ComputedStringAttribute("id"), + RequiredStringAttribute("location"), + RequiredStringAttribute("name"), + }, + }, + }, + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "value_we_dont_always_need", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + }, + }, + } +} + +// This example resource, on update plans, will plan a different identity to test that +// our testing framework assertions catch an identity that differs after import/refresh. +func examplecloudResourceWithChangingIdentity() testprovider.Resource { + exampleCloudResource := examplecloudResource() + + exampleCloudResource.PlanChangeFunc = func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + // Only on update + if !req.PriorState.IsNull() && !req.ProposedNewState.IsNull() { + resp.PlannedIdentity = teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "easteurope/someothervalue"), + }, + )) + } + } + + return exampleCloudResource +} diff --git a/helper/resource/importstate/import_block_as_first_step_test.go b/helper/resource/importstate/import_block_as_first_step_test.go new file mode 100644 index 000000000..e7dacef9f --- /dev/null +++ b/helper/resource/importstate/import_block_as_first_step_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_AsFirstStep(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ResourceName: "examplecloud_container.test", + ImportStateId: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + Config: `resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ImportStateConfigExact: true, + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("id"), knownvalue.StringExact("westeurope/somevalue")), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("name"), knownvalue.StringExact("somevalue")), + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("location"), knownvalue.StringExact("westeurope")), + }, + }, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go b/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go new file mode 100644 index 000000000..2464929dd --- /dev/null +++ b/helper/resource/importstate/import_block_for_resource_with_a_dependency_test.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestImportBlockForResourceWithADependency(t *testing.T) { + t.Parallel() + + config := ` +resource "examplecloud_zone" "zone" { + name = "example.net" +} + +resource "examplecloud_zone_record" "record" { + zone_id = examplecloud_zone.zone.id + name = "www" +} +` + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_zone": examplecloudZone(), + "examplecloud_zone_record": examplecloudZoneRecord(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: config, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ResourceName: "examplecloud_zone_record.record", + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_in_config_directory_test.go b/helper/resource/importstate/import_block_in_config_directory_test.go new file mode 100644 index 000000000..cf24a0240 --- /dev/null +++ b/helper/resource/importstate/import_block_in_config_directory_test.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_InConfigDirectory(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/1`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ConfigDirectory: config.StaticDirectory(`testdata/2`), + }, + }, + }) +} + +func TestImportBlock_InConfigDirectory_ConfigExactTrue(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigDirectory: config.StaticDirectory(`testdata/1`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + + // This content includes an import block with an ID so we will + // use the exact content + ConfigDirectory: config.StaticDirectory(`testdata/2_with_exact_import_config`), + ImportStateConfigExact: true, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_in_config_file_test.go b/helper/resource/importstate/import_block_in_config_file_test.go new file mode 100644 index 000000000..db762f2d3 --- /dev/null +++ b/helper/resource/importstate/import_block_in_config_file_test.go @@ -0,0 +1,105 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_InConfigFile(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ConfigFile: config.StaticFile(`testdata/2/examplecloud_container.tf`), + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_InConfigFile(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + ConfigFile: config.StaticFile(`testdata/2/examplecloud_container.tf`), + }, + }, + }) +} + +func TestImportBlock_InConfigFile_ConfigExactTrue(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`), + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + + // This content includes an import block with an ID so we will + // use the exact content + ConfigFile: config.StaticFile(`testdata/examplecloud_container_with_exact_import_config_with_id.tf`), + ImportStateConfigExact: true, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_with_id_test.go b/helper/resource/importstate/import_block_with_id_test.go new file mode 100644 index 000000000..c11754a6d --- /dev/null +++ b/helper/resource/importstate/import_block_with_id_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_WithID(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_WithID_ExpectError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + Config: ` + resource "examplecloud_container" "test" { + location = "eastus" + name = "somevalue" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op import operation, got.*\["update"\] action with plan(.?)`), + }, + }, + }) +} + +func TestImportBlock_WithID_FailWhenNotSupported(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_4_0), // ImportBlockWithId requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ResourceName: "examplecloud_container.test", + ExpectError: regexp.MustCompile(`Terraform 1.5.0`), + }, + }, + }) +} + +func TestImportBlock_WithID_SkipsDataSources(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + DataSources: map[string]testprovider.DataSource{ + "examplecloud_thing": examplecloudDataSource(), + }, + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + data "examplecloud_thing" "test" {} + resource "examplecloud_thing" "test" { + name = "somevalue" + location = "westeurope" + } + `, + }, + { + ResourceName: "examplecloud_thing.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if len(is) > 1 { + return fmt.Errorf("expected 1 state, got: %d", len(is)) + } + + return nil + }, + }, + }, + }) +} + +func TestImportBlock_WithID_WithBlankOptionalAttribute_GeneratesCorrectPlan(t *testing.T) { + /* + This test tries to imitate a real world example of behaviour we often see in the AzureRM provider which requires + the use of `ImportStateVerifyIgnore` when testing the import of a resource using the import command. + + A sensitive field e.g. a password can be supplied on create but isn't returned in the API response on a subsequent + read, resulting in a different value for password in the two states. + + In the AzureRM provider this is usually handled one of two ways, both requiring `ImportStateVerifyIgnore` to make + the test pass: + + 1. Property doesn't get set in the read + * in pluginSDK at create the config gets written to state because that's what we're expecting + * the subsequent read updates the values to create a post-apply diff and update computed values + * since we don't do anything to the property in the read the imported resource's state has the password missing + compared to the created resource's state + + 2. We retrieve the value from config and set that into state + * the config isn't available at import time using only the import command (I think?) so there is nothing to + retrieve and set into state when importing + + I also need to omit the `password` in the import config, otherwise the value in the config is used when importing the + with an import block and the test ends up passing regardless of whether `ImportStateVerifyIgnore` has been specified or not + */ + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "name", + Type: tftypes.String, + Required: true, + }, + { + Name: "password", + Type: tftypes.String, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + name = "somename" + password = "somevalue" + }`, + }, + { + Config: ` + terraform { + required_providers { + examplecloud = { + source = "registry.terraform.io/hashicorp/examplecloud" + } + } + } + + resource "examplecloud_container" "test" { + name = "somename" + } + + import { + to = examplecloud_container.test + id = "sometestid" + + }`, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + }, + }, + }) +} + +func TestImportBlock_WithID_WithBlankComputedAttribute_GeneratesCorrectPlan(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, "somevalue"), + }, + ), + }, + ImportStateResponse: &resource.ImportStateResponse{ + State: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "name": tftypes.String, + "password": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "sometestid"), + "name": tftypes.NewValue(tftypes.String, "somename"), + "password": tftypes.NewValue(tftypes.String, nil), // this simulates an absent property when importing + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "password", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_container" "test" {}`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + }, + }, + }) +} + +func TestImportBlock_WithID_WithExternalProvider(t *testing.T) { + t.Parallel() + + config := ` +resource "random_string" "mystery_message" { + length = 31 +} +` + + configWithImportBlock := config + ` +import { + to = random_string.mystery_message + id = "It was a dark and stormy night." +} +` + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later + }, + ExternalProviders: map[string]r.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + }, + Steps: []r.TestStep{ + { + Config: config, + }, + { + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + Config: configWithImportBlock, + ResourceName: "random_string.mystery_message", + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue( + "random_string.mystery_message", + tfjsonpath.New("result"), + knownvalue.StringExact("It was a dark and stormy night.")), + }, + }, + }, + }, + }) +} diff --git a/helper/resource/importstate/import_block_with_resource_identity_test.go b/helper/resource/importstate/import_block_with_resource_identity_test.go new file mode 100644 index 000000000..c71a8b578 --- /dev/null +++ b/helper/resource/importstate/import_block_with_resource_identity_test.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportBlock_WithResourceIdentity(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_NullAttribute(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithNullIdentityAttr(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity("examplecloud_container.test", map[string]knownvalue.Check{ + "id": knownvalue.StringExact("westeurope/somevalue"), + "value_we_dont_always_need": knownvalue.Null(), // This value will not be brought over to import config + }), + }, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_WithEveryType(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithEveryIdentitySchemaType(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + cabinet = "A1" + unit = 14 + tags = ["storage", "fast"] + active = true + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_ChangingIdentityError(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResourceWithChangingIdentity(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + // The plan following the import will produce a different identity value then test step 1 + ExpectError: regexp.MustCompile(`expected identity values map\[id:westeurope/somevalue\], got map\[id:easteurope/someothervalue\]`), + }, + }, + }) +} + +func TestImportBlock_WithResourceIdentity_RequiresVersion1_12_0(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), // ImportBlockWithResourceIdentity requires Terraform 1.12.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "examplecloud_container" "test" { + location = "westeurope" + name = "somevalue" + }`, + }, + { + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithResourceIdentity, + ExpectError: regexp.MustCompile(`Terraform 1.12.0\S* or later`), + }, + }, + }) +} diff --git a/helper/resource/importstate/import_command_as_first_step_test.go b/helper/resource/importstate/import_command_as_first_step_test.go new file mode 100644 index 000000000..14db18828 --- /dev/null +++ b/helper/resource/importstate/import_command_as_first_step_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestImportCommand_AsFirstStep(t *testing.T) { + t.Parallel() + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories needs Terraform 1.0.0 or later + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_container": examplecloudResource(), + }, + }), + }, + Steps: []r.TestStep{ + { + ResourceName: "examplecloud_container.test", + ImportStateId: "examplecloud_container.test", + ImportState: true, + Config: `resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" + }`, + ImportStatePersist: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + if len(states) != 1 { + return fmt.Errorf("expected 1 state; got %d", len(states)) + } + if states[0].ID != "westeurope/somevalue" { + return fmt.Errorf("unexpected ID: %s", states[0].ID) + } + if states[0].Attributes["name"] != "somevalue" { + return fmt.Errorf("unexpected name: %s", states[0].Attributes["name"]) + } + if states[0].Attributes["location"] != "westeurope" { + return fmt.Errorf("unexpected location: %s", states[0].Attributes["location"]) + } + return nil + }, + }, + }, + }) +} diff --git a/helper/resource/testing_new_import_state_test.go b/helper/resource/importstate/import_command_with_id_test.go similarity index 94% rename from helper/resource/testing_new_import_state_test.go rename to helper/resource/importstate/import_command_with_id_test.go index 9710a1bf6..34e211b67 100644 --- a/helper/resource/testing_new_import_state_test.go +++ b/helper/resource/importstate/import_command_with_id_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package resource +package importstate_test import ( "fmt" @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/datasource" "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" @@ -19,10 +20,10 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestTest_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) { +func TestImportCommand_ImportStateCheckSkipsDataSources(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories }, @@ -100,7 +101,7 @@ func TestTest_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) { }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ { Config: ` data "examplecloud_thing" "test" {} @@ -122,10 +123,10 @@ func TestTest_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) { }) } -func TestTest_TestStep_ImportStateVerify(t *testing.T) { +func TestImportCommand_ImportStateVerify(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories }, @@ -183,7 +184,7 @@ func TestTest_TestStep_ImportStateVerify(t *testing.T) { }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "test" {}`, }, @@ -196,10 +197,10 @@ func TestTest_TestStep_ImportStateVerify(t *testing.T) { }) } -func TestTest_TestStep_ImportStateVerifyIgnore(t *testing.T) { +func TestImportCommand_ImportStateVerify_Ignore(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories }, @@ -266,7 +267,7 @@ func TestTest_TestStep_ImportStateVerifyIgnore(t *testing.T) { }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ { Config: `resource "examplecloud_thing" "test" {}`, }, @@ -280,10 +281,10 @@ func TestTest_TestStep_ImportStateVerifyIgnore(t *testing.T) { }) } -func TestTest_TestStep_ExpectError_ImportState(t *testing.T) { +func TestImportCommand_ExpectError(t *testing.T) { t.Parallel() - UnitTest(t, TestCase{ + r.UnitTest(t, r.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories }, @@ -329,7 +330,7 @@ func TestTest_TestStep_ExpectError_ImportState(t *testing.T) { }, }), }, - Steps: []TestStep{ + Steps: []r.TestStep{ { Config: `resource "test_resource" "test" {}`, ImportStateId: "invalid time string", diff --git a/helper/resource/importstate/testdata/1/examplecloud_container.tf b/helper/resource/importstate/testdata/1/examplecloud_container.tf new file mode 100644 index 000000000..ccfb698e6 --- /dev/null +++ b/helper/resource/importstate/testdata/1/examplecloud_container.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} diff --git a/helper/resource/importstate/testdata/2/examplecloud_container.tf b/helper/resource/importstate/testdata/2/examplecloud_container.tf new file mode 100644 index 000000000..ccfb698e6 --- /dev/null +++ b/helper/resource/importstate/testdata/2/examplecloud_container.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} diff --git a/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf b/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf new file mode 100644 index 000000000..f7e9411f9 --- /dev/null +++ b/helper/resource/importstate/testdata/2_with_exact_import_config/examplecloud_container.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} + +import { + to = examplecloud_container.test + id = "examplecloud_container.test" +} diff --git a/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf b/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf new file mode 100644 index 000000000..f7e9411f9 --- /dev/null +++ b/helper/resource/importstate/testdata/examplecloud_container_with_exact_import_config_with_id.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "examplecloud_container" "test" { + name = "somevalue" + location = "westeurope" +} + +import { + to = examplecloud_container.test + id = "examplecloud_container.test" +} diff --git a/helper/resource/importstate/types_test.go b/helper/resource/importstate/types_test.go new file mode 100644 index 000000000..8532b40da --- /dev/null +++ b/helper/resource/importstate/types_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package importstate_test + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func RequiredBoolAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Bool, + Required: true, + } +} + +func OptionalComputedListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Optional: true, + Computed: true, + } +} + +func RequiredListAttribute(name string, elementType tftypes.Type) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.List{ElementType: elementType}, + Required: true, + } +} + +func RequiredNumberAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.Number, + Required: true, + } +} + +func ComputedStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Computed: true, + } +} + +func OptionalStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Optional: true, + } +} + +func RequiredStringAttribute(name string) *tfprotov6.SchemaAttribute { + return &tfprotov6.SchemaAttribute{ + Name: name, + Type: tftypes.String, + Required: true, + } +} diff --git a/helper/resource/plugin.go b/helper/resource/plugin.go index 5c92f3ab9..6e16e1613 100644 --- a/helper/resource/plugin.go +++ b/helper/resource/plugin.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -113,7 +114,33 @@ type providerFactories struct { protov6 protov6ProviderFactories } -func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error { +func runProviderCommandCreatePlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) error { + t.Helper() + + fn := func() error { + return wd.CreatePlan(ctx) + } + return runProviderCommand(ctx, t, wd, factories, fn) +} + +func runProviderCommandSavedPlan(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories) (*tfjson.Plan, error) { + t.Helper() + + var plan *tfjson.Plan + fn := func() error { + var err error + plan, err = wd.SavedPlan(ctx) + return err + } + err := runProviderCommand(ctx, t, wd, factories, fn) + if err != nil { + return nil, err + } + + return plan, nil +} + +func runProviderCommand(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, factories *providerFactories, f func() error) error { // don't point to this as a test failure location // point to whatever called it t.Helper() @@ -178,14 +205,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl providerName = strings.TrimPrefix(providerName, "terraform-provider-") providerAddress := getProviderAddr(providerName) - logging.HelperResourceDebug(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created sdkv2 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -215,14 +242,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started sdkv2 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -272,14 +299,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } } - logging.HelperResourceDebug(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created tfprotov5 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -303,14 +330,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started tfprotov5 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -361,14 +388,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } } - logging.HelperResourceDebug(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Creating tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) provider, err := factory() if err != nil { return fmt.Errorf("unable to create provider %q from factory: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Created tfprotov6 provider instance", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) // keep track of the running factory, so we can make sure it's // shut down. @@ -388,14 +415,14 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl ProviderAddr: providerAddress, } - logging.HelperResourceDebug(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Starting tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) config, closeCh, err := plugin.DebugServe(ctx, opts) if err != nil { return fmt.Errorf("unable to serve provider %q: %w", providerName, err) } - logging.HelperResourceDebug(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) + logging.HelperResourceTrace(ctx, "Started tfprotov6 provider instance server", map[string]interface{}{logging.KeyProviderAddress: providerAddress}) tfexecConfig := tfexec.ReattachConfig{ Protocol: config.Protocol, @@ -441,7 +468,7 @@ func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *pl } logging.HelperResourceTrace(ctx, "Called wrapped Terraform CLI command") - logging.HelperResourceDebug(ctx, "Stopping providers") + logging.HelperResourceTrace(ctx, "Stopping providers") // cancel the servers so they'll return. Otherwise, this closeCh won't // get closed, and we'll hang here. diff --git a/helper/resource/plugin_test.go b/helper/resource/plugin_test.go index e3b159963..62823efd7 100644 --- a/helper/resource/plugin_test.go +++ b/helper/resource/plugin_test.go @@ -250,44 +250,38 @@ func TestRunProviderCommand(t *testing.T) { funcCalled := false helper := plugintest.AutoInitProviderHelper(ctx, currentDir) - err = runProviderCommand( - ctx, - t, - func() error { - funcCalled = true - return nil - }, - helper.RequireNewWorkingDir(ctx, t, ""), - &providerFactories{ - legacy: map[string]func() (*schema.Provider, error){ - "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - "examplecloud_thing": { - CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - d.SetId("id") - - return nil - }, - DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "id": { - Computed: true, - Type: schema.TypeString, - }, + err = runProviderCommand(ctx, t, helper.RequireNewWorkingDir(ctx, t, ""), &providerFactories{ + legacy: map[string]func() (*schema.Provider, error){ + "examplecloud": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "examplecloud_thing": { + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Computed: true, + Type: schema.TypeString, }, }, }, - }, nil - }, + }, + }, nil }, }, - ) + }, func() error { + funcCalled = true + return nil + }) if err != nil { t.Fatal(err) diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 9e1961a46..9e7192e4e 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -454,6 +454,37 @@ type ExternalProvider struct { Source string // the provider source } +type ImportStateKind byte + +const ( + // ImportCommandWithID tests import by using the ID string with the `terraform import` command + ImportCommandWithID ImportStateKind = iota + + // ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command + ImportBlockWithID + + // ImportBlockWithResourceIdentity imports the state using an import block with a resource identity + ImportBlockWithResourceIdentity +) + +// plannable reports whether this kind indicates the use of plannable import blocks +func (kind ImportStateKind) plannable() bool { + return kind == ImportBlockWithID || kind == ImportBlockWithResourceIdentity +} + +// resourceIdentity reports whether this kind indicates the use of resource identity in import blocks +func (kind ImportStateKind) resourceIdentity() bool { + return kind == ImportBlockWithResourceIdentity +} + +func (kind ImportStateKind) String() string { + return map[ImportStateKind]string{ + ImportCommandWithID: "ImportCommandWithID", + ImportBlockWithID: "ImportBlockWithID", + ImportBlockWithResourceIdentity: "ImportBlockWithResourceIdentity", + }[kind] +} + // TestStep is a single apply sequence of a test, done within the // context of a state. // @@ -545,6 +576,16 @@ type TestStep struct { // otherwise an error will be returned. ConfigFile config.TestStepConfigFunc + // ImportStateConfigExact indicates that the test framework should use the exact + // content of the Config, ConfigFile, or ConfigDirectory inputs and should + // not modify it at test run time. + // + // The default is false. At test run time, the test framework will generate + // specific kinds of configuration, such as import blocks, and append them + // to the given Config, ConfigFile, or ConfigDirectory inputs. Using this + // default improves test readability and removes duplication of setup. + ImportStateConfigExact bool + // ConfigVariables is a map defining variables for use in conjunction // with Terraform configuration. If this map is populated then it // will be used to assemble an *.auto.tfvars.json which will be @@ -633,6 +674,8 @@ type TestStep struct { // ID of that resource. ImportState bool + ImportStateKind ImportStateKind + // ImportStateId is the ID to perform an ImportState operation with. // This is optional. If it isn't set, then the resource ID is automatically // determined by inspecting the state for ResourceName's ID. @@ -666,6 +709,13 @@ type TestStep struct { // Terraform version specific logic in provider testing. ImportStateCheck ImportStateCheckFunc + // ImportPlanChecks allows assertions to be made against the plan file at different points of a plannable import test using a plan check. + // Custom plan checks can be created by implementing the [PlanCheck] interface, or by using a PlanCheck implementation from the provided [plancheck] package + // + // [PlanCheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck#PlanCheck + // [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck + ImportPlanChecks ImportPlanChecks + // ImportStateVerify, if true, will also check that the state values // that are finally put into the state after import match for all the // IDs returned by the Import. Note that this checks for strict equality @@ -797,6 +847,13 @@ type ConfigPlanChecks struct { PostApplyPostRefresh []plancheck.PlanCheck } +// ImportPlanChecks defines the different points in an Import TestStep when plan checks can be run. +type ImportPlanChecks struct { + // PreApply runs all plan checks in the slice. This occurs after the plan of an Import test is computed. This slice cannot be populated + // with TestStep.PlanOnly, as there is no PreApply plan run with that flag set. All errors by plan checks in this slice are aggregated, reported, and will result in a test failure. + PreApply []plancheck.PlanCheck +} + // RefreshPlanChecks defines the different points in a Refresh TestStep when plan checks can be run. type RefreshPlanChecks struct { // PostRefresh runs all plan checks in the slice. This occurs after the refresh of the Refresh test is run. @@ -824,11 +881,6 @@ func ParallelTest(t testing.T, c TestCase) { // set to some non-empty value. This is to avoid test cases surprising // a user by creating real resources. // -// Tests will fail unless the verbose flag (`go test -v`, or explicitly -// the "-test.v" flag) is set. Because some acceptance tests take quite -// long, we require the verbose flag so users are able to see progress -// output. -// // Use the ParallelTest() function to automatically set (*testing.T).Parallel() // to enable testing concurrency. Use the UnitTest() function to automatically // set the TestCase type IsUnitTest field. @@ -916,11 +968,7 @@ func Test(t testing.T, c TestCase) { // This is done after creating the helper because a working directory is required // to retrieve the Terraform version. if c.TerraformVersionChecks != nil { - logging.HelperResourceDebug(ctx, "Calling TestCase Terraform version checks") - runTFVersionChecks(ctx, t, helper.TerraformVersion(), c.TerraformVersionChecks) - - logging.HelperResourceDebug(ctx, "Called TestCase Terraform version checks") } runNewTest(ctx, t, c, helper) @@ -940,17 +988,17 @@ func UnitTest(t testing.T, c TestCase) { Test(t, c) } -func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { +func testResource(name string, state *terraform.State) (*terraform.ResourceState, error) { for _, m := range state.Modules { if len(m.Resources) > 0 { - if v, ok := m.Resources[c.ResourceName]; ok { + if v, ok := m.Resources[name]; ok { return v, nil } } } return nil, fmt.Errorf( - "Resource specified by ResourceName couldn't be found: %s", c.ResourceName) + "Resource specified by ResourceName couldn't be found: %s", name) } // ComposeTestCheckFunc lets you compose multiple TestCheckFuncs into diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 0a7c7e7f7..ea66b67be 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -27,9 +27,9 @@ import ( func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { t.Helper() - err := runProviderCommand(ctx, t, func() error { + err := runProviderCommand(ctx, t, wd, providers, func() error { return wd.Destroy(ctx) - }, wd, providers) + }) if err != nil { return err } @@ -67,13 +67,13 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest var statePreDestroy *terraform.State var err error - err = runProviderCommand(ctx, t, func() error { - statePreDestroy, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, statePreDestroy, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { logging.HelperResourceError(ctx, "Error retrieving state, there may be dangling resources", @@ -116,9 +116,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("TestCase error setting provider configuration: %s", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.Init(ctx) - }, wd, providers) + }) if err != nil { logging.HelperResourceError(ctx, @@ -129,11 +129,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - logging.HelperResourceDebug(ctx, "Starting TestSteps") - // use this to track last step successfully applied // acts as default for import tests - var appliedCfg teststep.Config + var appliedCfg string var stepNumber int for stepIndex, step := range c.Steps { @@ -249,7 +247,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest File: step.ConfigFile, Raw: rawCfg, TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, + StepNumber: stepNumber, TestName: t.Name(), }, }.Exec() @@ -266,15 +264,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("TestStep %d/%d error setting test provider configuration: %s", stepNumber, len(c.Steps), err) } - err = runProviderCommand( - ctx, - t, - func() error { - return wd.Init(ctx) - }, - wd, - providers, - ) + err = runProviderCommand(ctx, t, wd, providers, func() error { + return wd.Init(ctx) + }) if err != nil { logging.HelperResourceError(ctx, @@ -289,7 +281,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest if step.ImportState { logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") - err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepIndex) + err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers, stepNumber) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { @@ -426,7 +418,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - mergedConfig, err := step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) + appliedCfg, err = step.mergedConfig(ctx, c, hasTerraformBlock, hasProviderBlock, helper.TerraformVersion()) if err != nil { logging.HelperResourceError(ctx, @@ -436,18 +428,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Error generating merged configuration: %s", err) } - confRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: mergedConfig, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() - - appliedCfg = teststep.Configuration(confRequest) - logging.HelperResourceDebug(ctx, "Finished TestStep") continue @@ -461,18 +441,18 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } -func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*terraform.State, error) { +func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*tfjson.State, *terraform.State, error) { t.Helper() jsonState, err := wd.State(ctx) if err != nil { - return nil, err + return nil, nil, err } state, err := shimStateFromJson(jsonState) if err != nil { t.Fatal(err) } - return state, nil + return jsonState, state, nil } func stateIsEmpty(state *terraform.State) bool { @@ -582,17 +562,17 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. }() // Refresh! - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { err = wd.Refresh(ctx) if err != nil { t.Fatalf("Error running terraform refresh: %s", err) } - state, err = getState(ctx, t, wd) + _, state, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return err } diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 1456f7fba..babaf8410 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -102,18 +102,23 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint logging.HelperResourceDebug(ctx, "Running Terraform CLI plan and apply") // Plan! - err := runProviderCommand(ctx, t, func() error { + err := runProviderCommand(ctx, t, wd, providers, func() error { var opts []tfexec.PlanOption if step.Destroy { opts = append(opts, tfexec.Destroy(true)) } - if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } } return wd.CreatePlan(ctx, opts...) - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error running pre-apply plan: %w", err) } @@ -121,11 +126,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // Run pre-apply plan checks if len(step.ConfigPlanChecks.PreApply) > 0 { var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving pre-apply plan: %w", err) } @@ -150,21 +155,21 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // the shim logic will return an error such as: // // Failed to marshal state to json: schema version 0 for null_resource.test in state does not match version 1 from the provider - err := runProviderCommand(ctx, t, func() error { + err := runProviderCommand(ctx, t, wd, providers, func() error { return wd.Refresh(ctx) - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error running pre-apply refresh: %w", err) } - err = runProviderCommand(ctx, t, func() error { - stateBeforeApplication, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, stateBeforeApplication, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving pre-apply state: %w", err) @@ -172,7 +177,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } // Apply the diff, creating real resources - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var opts []tfexec.ApplyOption if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral { @@ -180,7 +185,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } return wd.Apply(ctx, opts...) - }, wd, providers) + }) if err != nil { if step.Destroy { return fmt.Errorf("Error running destroy: %w", err) @@ -199,13 +204,13 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } else { var state *terraform.State - err := runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) + err := runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving state after apply: %w", err) @@ -221,11 +226,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint if len(step.ConfigStateChecks) > 0 { var state *tfjson.State - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error state, err = wd.State(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving post-apply, post-refresh state: %w", err) @@ -242,7 +247,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint logging.HelperResourceDebug(ctx, "Running Terraform CLI plan to check for perpetual differences") // do a plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { opts := []tfexec.PlanOption{ tfexec.Refresh(false), } @@ -250,12 +255,17 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint opts = append(opts, tfexec.Destroy(true)) } - if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } } return wd.CreatePlan(ctx, opts...) - }, wd, providers) + }) if err != nil { if step.PlanOnly { return fmt.Errorf("Error running non-refresh plan: %w", err) @@ -265,11 +275,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { if step.PlanOnly { return fmt.Errorf("Error reading saved non-refresh plan: %w", err) @@ -292,11 +302,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error reading saved human-readable non-refresh plan output: %w", err) } @@ -309,7 +319,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } // do another plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var opts []tfexec.PlanOption if step.Destroy { opts = append(opts, tfexec.Destroy(true)) @@ -319,12 +329,17 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint } } - if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { - opts = append(opts, tfexec.AllowDeferral(true)) + if c.AdditionalCLIOptions != nil { + if c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + if c.AdditionalCLIOptions.Plan.NoRefresh { + opts = append(opts, tfexec.Refresh(false)) + } } return wd.CreatePlan(ctx, opts...) - }, wd, providers) + }) if err != nil { if step.PlanOnly { return fmt.Errorf("Error running refresh plan: %w", err) @@ -333,11 +348,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return fmt.Errorf("Error running post-apply refresh plan: %w", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { if step.PlanOnly { return fmt.Errorf("Error reading refresh plan: %w", err) @@ -357,11 +372,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // check if plan is empty if !planIsEmpty(plan, helper.TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error reading human-readable refresh plan output: %w", err) } @@ -383,13 +398,13 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var state *terraform.State - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, state, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { return err diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 7dbc0b800..f2e265d56 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -5,51 +5,54 @@ package resource import ( "context" + "encoding/json" "fmt" "reflect" "strings" + "github.com/hashicorp/go-version" + + tfjson "github.com/hashicorp/terraform-json" + "github.com/google/go-cmp/cmp" "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/internal/teststep" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg teststep.Config, providers *providerFactories, stepIndex int) error { +func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, testCaseWorkingDir *plugintest.WorkingDir, step TestStep, cfgRaw string, providers *providerFactories, stepNumber int) error { t.Helper() - configRequest := teststep.PrepareConfigurationRequest{ - Directory: step.ConfigDirectory, - File: step.ConfigFile, - Raw: step.Config, - TestStepConfigRequest: config.TestStepConfigRequest{ - StepNumber: stepIndex + 1, - TestName: t.Name(), - }, - }.Exec() + // step.ImportStateKind implicitly defaults to the zero-value (ImportCommandWithID) for backward compatibility + kind := step.ImportStateKind + importStatePersist := step.ImportStatePersist - testStepConfig := teststep.Configuration(configRequest) + if err := importStatePreconditions(t, helper, step); err != nil { + return err + } - if step.ResourceName == "" { + resourceName := step.ResourceName + if resourceName == "" { t.Fatal("ResourceName is required for an import state test") } // get state from check sequence var state *terraform.State + var stateJSON *tfjson.State var err error - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, testCaseWorkingDir, providers, func() error { + stateJSON, state, err = getState(ctx, t, testCaseWorkingDir) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } @@ -78,7 +81,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest default: logging.HelperResourceTrace(ctx, "Using resource identifier for import identifier") - resource, err := testResource(step, state) + resource, err := testResource(resourceName, state) if err != nil { t.Fatal(err) } @@ -93,87 +96,176 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest logging.HelperResourceTrace(ctx, fmt.Sprintf("Using import identifier: %s", importId)) - // Create working directory for import tests - if testStepConfig == nil { - logging.HelperResourceTrace(ctx, "Using prior TestStep Config for import") + var priorIdentityValues map[string]any - testStepConfig = cfg - if testStepConfig == nil { - t.Fatal("Cannot import state with no specified config") + if kind.plannable() && kind.resourceIdentity() { + priorIdentityValues = identityValuesFromStateValues(stateJSON.Values, resourceName) + if len(priorIdentityValues) == 0 { + return fmt.Errorf("importing resource %s: expected prior state to have resource identity values, got none", resourceName) } } - var importWd *plugintest.WorkingDir + var inlineConfig string + if step.Config != "" { + inlineConfig = step.Config + } else { + inlineConfig = cfgRaw + } + testStepConfigRequest := config.TestStepConfigRequest{ + StepNumber: stepNumber, + TestName: t.Name(), + } + testStepConfig := teststep.Configuration(teststep.PrepareConfigurationRequest{ + Directory: step.ConfigDirectory, + File: step.ConfigFile, + Raw: inlineConfig, + TestStepConfigRequest: testStepConfigRequest, + }.Exec()) + + switch { + case step.ImportStateConfigExact: + break + + case kind.plannable() && kind.resourceIdentity(): + testStepConfig = appendImportBlockWithIdentity(testStepConfig, resourceName, priorIdentityValues) - // Use the same working directory to persist the state from import - if step.ImportStatePersist { - importWd = wd + case kind.plannable(): + testStepConfig = appendImportBlock(testStepConfig, resourceName, importId) + } + + if testStepConfig == nil { + t.Fatal("Cannot import state with no specified config") + } + + var workingDir *plugintest.WorkingDir + if importStatePersist { + workingDir = testCaseWorkingDir } else { - importWd = helper.RequireNewWorkingDir(ctx, t, "") - defer importWd.Close() + workingDir = helper.RequireNewWorkingDir(ctx, t, "") + defer workingDir.Close() } - err = importWd.SetConfig(ctx, testStepConfig, step.ConfigVariables) + err = workingDir.SetConfig(ctx, testStepConfig, step.ConfigVariables) if err != nil { t.Fatalf("Error setting test config: %s", err) } - logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") + if kind.plannable() { + if stepNumber > 1 { + err = workingDir.CopyState(ctx, testCaseWorkingDir.StateFilePath()) + if err != nil { + t.Fatalf("copying state: %s", err) + } + + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.RemoveResource(ctx, resourceName) + }) + if err != nil { + t.Fatalf("removing resource %s from copied state: %s", resourceName, err) + } + } + } - if !step.ImportStatePersist { - err = runProviderCommand(ctx, t, func() error { - return importWd.Init(ctx) - }, importWd, providers) + if !importStatePersist { + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.Init(ctx) + }) if err != nil { t.Fatalf("Error running init: %s", err) } } - err = runProviderCommand(ctx, t, func() error { - return importWd.Import(ctx, step.ResourceName, importId) - }, importWd, providers) + if kind.plannable() { + return testImportBlock(ctx, t, workingDir, providers, resourceName, step, priorIdentityValues) + } else { + return testImportCommand(ctx, t, workingDir, providers, resourceName, importId, step, state) + } +} + +func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, step TestStep, priorIdentityValues map[string]any) error { + kind := step.ImportStateKind + + err := runProviderCommandCreatePlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("generating plan with import config: %s", err) + } + + plan, err := runProviderCommandSavedPlan(ctx, t, workingDir, providers) + if err != nil { + return fmt.Errorf("reading generated plan with import config: %s", err) + } + + logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges))) + + // Verify reasonable things about the plan + var resourceChangeUnderTest *tfjson.ResourceChange + + if len(plan.ResourceChanges) == 0 { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + for _, change := range plan.ResourceChanges { + if change.Address == resourceName { + resourceChangeUnderTest = change + } + } + + if resourceChangeUnderTest == nil || resourceChangeUnderTest.Change == nil || resourceChangeUnderTest.Change.Actions == nil { + return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName) + } + + change := resourceChangeUnderTest.Change + actions := change.Actions + importing := change.Importing + + switch { + case importing == nil: + return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + + case !actions.NoOp(): + return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) + } + + if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil { + return err + } + + if kind.resourceIdentity() { + newIdentityValues := identityValuesFromStateValues(plan.PlannedValues, resourceName) + if !cmp.Equal(priorIdentityValues, newIdentityValues) { + return fmt.Errorf("importing resource %s: expected identity values %v, got %v", resourceName, priorIdentityValues, newIdentityValues) + } + } + + return nil +} + +func testImportCommand(ctx context.Context, t testing.T, workingDir *plugintest.WorkingDir, providers *providerFactories, resourceName string, importId string, step TestStep, state *terraform.State) error { + err := runProviderCommand(ctx, t, workingDir, providers, func() error { + return workingDir.Import(ctx, resourceName, importId) + }) if err != nil { return err } var importState *terraform.State - err = runProviderCommand(ctx, t, func() error { - importState, err = getState(ctx, t, importWd) + err = runProviderCommand(ctx, t, workingDir, providers, func() error { + _, importState, err = getState(ctx, t, workingDir) if err != nil { return err } return nil - }, importWd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } + logging.HelperResourceDebug(ctx, fmt.Sprintf("State after import: %d resources in the root module", len(importState.RootModule().Resources))) + // Go through the imported state and verify if step.ImportStateCheck != nil { logging.HelperResourceTrace(ctx, "Using TestStep ImportStateCheck") - - var states []*terraform.InstanceState - for address, r := range importState.RootModule().Resources { - if strings.HasPrefix(address, "data.") { - continue - } - - if r.Primary == nil { - continue - } - - is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage - is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type - states = append(states, is) - } - - logging.HelperResourceDebug(ctx, "Calling TestStep ImportStateCheck") - - if err := step.ImportStateCheck(states); err != nil { - t.Fatal(err) - } - - logging.HelperResourceDebug(ctx, "Called TestStep ImportStateCheck") + runImportStateCheckFunction(ctx, t, importState, step) } // Verify that all the states match @@ -316,3 +408,162 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest return nil } + +func appendImportBlock(config teststep.Config, resourceName string, importID string) teststep.Config { + return config.Append( + fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` id = %q`+"\n"+ + `}`, + resourceName, importID)) +} + +func appendImportBlockWithIdentity(config teststep.Config, resourceName string, identityValues map[string]any) teststep.Config { + configBuilder := strings.Builder{} + configBuilder.WriteString(fmt.Sprintf(``+"\n"+ + `import {`+"\n"+ + ` to = %s`+"\n"+ + ` identity = {`+"\n", + resourceName)) + + for k, v := range identityValues { + // It's valid for identity attributes to be null, we can just omit it from config + if v == nil { + continue + } + + switch v := v.(type) { + case bool: + configBuilder.WriteString(fmt.Sprintf(` %q = %t`+"\n", k, v)) + + case []any: + var quotedV []string + for _, v := range v { + quotedV = append(quotedV, fmt.Sprintf(`%q`, v)) + } + configBuilder.WriteString(fmt.Sprintf(` %q = [%s]`+"\n", k, strings.Join(quotedV, ", "))) + + case json.Number: + configBuilder.WriteString(fmt.Sprintf(` %q = %s`+"\n", k, v)) + + case string: + configBuilder.WriteString(fmt.Sprintf(` %q = %q`+"\n", k, v)) + + default: + panic(fmt.Sprintf("unexpected type %T for identity value %q", v, k)) + } + } + + configBuilder.WriteString(` }` + "\n") + configBuilder.WriteString(`}` + "\n") + + return config.Append(configBuilder.String()) +} + +func importStatePreconditions(t testing.T, helper *plugintest.Helper, step TestStep) error { + t.Helper() + + kind := step.ImportStateKind + versionUnderTest := *helper.TerraformVersion().Core() + resourceIdentityMinimumVersion := version.Must(version.NewVersion("1.12.0")) + + // Instead of calling [t.Fatal], we return an error. This package's unit tests can use [TestStep.ExpectError] to match + // on the error message. An alternative, [plugintest.TestExpectTFatal], does not have access to logged error messages, + // so it is open to false positives on this complex code path. + // + // Multiple cases may match, so check the most specific cases first + switch { + case kind.resourceIdentity() && versionUnderTest.LessThan(resourceIdentityMinimumVersion): + return fmt.Errorf( + `ImportState steps using resource identity require Terraform 1.12.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && versionUnderTest.LessThan(tfversion.Version1_5_0): + return fmt.Errorf( + `ImportState steps using plannable import blocks require Terraform 1.5.0 or later. Either ` + + `upgrade the Terraform version running the test or add a ` + "`TerraformVersionChecks`" + ` to ` + + `the test case to skip this test.` + "\n\n" + + `https://developer.hashicorp.com/terraform/plugin/testing/acceptance-tests/tfversion-checks#skip-version-checks`) + + case kind.plannable() && step.ImportStatePersist: + return fmt.Errorf(`ImportStatePersist is not supported with plannable import blocks`) + + case kind.plannable() && step.ImportStateVerify: + return fmt.Errorf(`ImportStateVerify is not supported with plannable import blocks`) + } + + return nil +} + +func resourcesFromState(stateValues *tfjson.StateValues) []*tfjson.StateResource { + if stateValues == nil || stateValues.RootModule == nil { + return []*tfjson.StateResource{} + } + + return stateValues.RootModule.Resources +} + +func identityValuesFromStateValues(stateValues *tfjson.StateValues, resourceName string) map[string]any { + var resource *tfjson.StateResource + resources := resourcesFromState(stateValues) + + for _, r := range resources { + if r.Address == resourceName { + resource = r + break + } + } + + if resource == nil || len(resource.IdentityValues) == 0 { + return map[string]any{} + } + + return resource.IdentityValues +} + +func runImportStateCheckFunction(ctx context.Context, t testing.T, importState *terraform.State, step TestStep) { + t.Helper() + + var states []*terraform.InstanceState + for address, r := range importState.RootModule().Resources { + if strings.HasPrefix(address, "data.") { + continue + } + + if r.Primary == nil { + continue + } + + is := r.Primary.DeepCopy() //nolint:staticcheck // legacy usage + is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type + states = append(states, is) + } + + logging.HelperResourceTrace(ctx, "Calling TestStep ImportStateCheck") + + if err := step.ImportStateCheck(states); err != nil { + t.Fatal(err) + } + + logging.HelperResourceTrace(ctx, "Called TestStep ImportStateCheck") +} + +func savedPlanRawStdout(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, providers *providerFactories) string { + t.Helper() + + var stdout string + + err := runProviderCommand(ctx, t, wd, providers, func() error { + var err error + stdout, err = wd.SavedPlanRawStdout(ctx) + return err + }) + + if err != nil { + return fmt.Sprintf("error retrieving formatted plan output: %s", err) + } + return stdout +} diff --git a/helper/resource/testing_new_refresh_state.go b/helper/resource/testing_new_refresh_state.go index 5c0e38758..b1971a289 100644 --- a/helper/resource/testing_new_refresh_state.go +++ b/helper/resource/testing_new_refresh_state.go @@ -21,32 +21,32 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo var err error // Explicitly ensure prior state exists before refresh. - err = runProviderCommand(ctx, t, func() error { - _, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, _, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.Refresh(ctx) - }, wd, providers) + }) if err != nil { return err } var refreshState *terraform.State - err = runProviderCommand(ctx, t, func() error { - refreshState, err = getState(ctx, t, wd) + err = runProviderCommand(ctx, t, wd, providers, func() error { + _, refreshState, err = getState(ctx, t, wd) if err != nil { return err } return nil - }, wd, providers) + }) if err != nil { t.Fatalf("Error getting state: %s", err) } @@ -63,19 +63,19 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo } // do a plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { return wd.CreatePlan(ctx) - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error running post-refresh plan: %w", err) } var plan *tfjson.Plan - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving post-refresh plan: %w", err) } @@ -90,11 +90,11 @@ func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.Wo if !planIsEmpty(plan, wd.GetHelper().TerraformVersion()) && !step.ExpectNonEmptyPlan { var stdout string - err = runProviderCommand(ctx, t, func() error { + err = runProviderCommand(ctx, t, wd, providers, func() error { var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providers) + }) if err != nil { return fmt.Errorf("Error retrieving formatted plan output: %w", err) } diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index 68a5c4621..6ad3c2588 100644 --- a/helper/resource/teststep_providers_test.go +++ b/helper/resource/teststep_providers_test.go @@ -3360,8 +3360,8 @@ func TestTest_TestStep_ProviderFactories_Import_External_With_Data_Source(t *tes Test(t, TestCase{ ExternalProviders: map[string]ExternalProvider{ - "http": { - Source: "registry.terraform.io/hashicorp/http", + "null": { + Source: "registry.terraform.io/hashicorp/null", }, "random": { Source: "registry.terraform.io/hashicorp/random", @@ -3369,28 +3369,23 @@ func TestTest_TestStep_ProviderFactories_Import_External_With_Data_Source(t *tes }, Steps: []TestStep{ { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" + Config: ` + data "null_data_source" "values" { + inputs = { + length = 12 } + } - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, + resource "random_string" "example" { + length = data.null_data_source.values.outputs["length"] + } + `, Check: extractResourceAttr("random_string.example", "id", &id), }, { - Config: `data "http" "example" { - url = "https://checkpoint-api.hashicorp.com/v1/check/terraform" - } - - resource "random_string" "example" { - length = length(data.http.example.response_headers) - }`, - ResourceName: "random_string.example", - ImportState: true, - ImportStateCheck: composeImportStateCheck( - testCheckResourceAttrInstanceState(&id, "length", "12"), - ), + ResourceName: "random_string.example", + ImportState: true, + ImportStateCheck: testCheckResourceAttrInstanceState(&id, "length", "12"), ImportStateVerify: true, }, }, diff --git a/internal/logging/context.go b/internal/logging/context.go index 0fe8002aa..5a3108451 100644 --- a/internal/logging/context.go +++ b/internal/logging/context.go @@ -11,22 +11,6 @@ import ( testing "github.com/mitchellh/go-testing-interface" ) -// InitContext creates SDK logger contexts when the provider is running in -// "production" (not under acceptance testing). The incoming context will -// already have the root SDK logger and root provider logger setup from -// terraform-plugin-go tf5server RPC handlers. -func InitContext(ctx context.Context) context.Context { - ctx = tfsdklog.NewSubsystem(ctx, SubsystemHelperSchema, - // All calls are through the HelperSchema* helper functions - tfsdklog.WithAdditionalLocationOffset(1), - tfsdklog.WithLevelFromEnv(EnvTfLogSdkHelperSchema), - // Propagate tf_req_id, tf_rpc, etc. fields - tfsdklog.WithRootFields(), - ) - - return ctx -} - // InitTestContext registers the terraform-plugin-log/tfsdklog test sink, // configures the standard library log package, and creates SDK logger // contexts. The incoming context is expected to be devoid of logging setup. diff --git a/internal/logging/context_test.go b/internal/logging/context_test.go index 1d1e85e3d..833b7b544 100644 --- a/internal/logging/context_test.go +++ b/internal/logging/context_test.go @@ -15,43 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/internal/logging" ) -func TestInitContext(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - - // Simulate root logger fields that would have been associated by - // terraform-plugin-go prior to the InitContext() call. - ctx = tfsdklog.SetField(ctx, "tf_rpc", "GetProviderSchema") - ctx = tfsdklog.SetField(ctx, "tf_req_id", "123-testing-123") - - ctx = logging.InitContext(ctx) - - logging.HelperSchemaTrace(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "trace", - "@message": "test message", - "@module": "sdk.helper_schema", - "tf_rpc": "GetProviderSchema", - "tf_req_id": "123-testing-123", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - func TestTestNameContext(t *testing.T) { t.Parallel() diff --git a/internal/logging/environment_variables.go b/internal/logging/environment_variables.go index 2ffc73eee..846cf67dd 100644 --- a/internal/logging/environment_variables.go +++ b/internal/logging/environment_variables.go @@ -19,9 +19,4 @@ const ( // level of SDK helper/resource loggers. Infers root SDK logging level, if // unset. EnvTfLogSdkHelperResource = "TF_LOG_SDK_HELPER_RESOURCE" - - // EnvTfLogSdkHelperSchema is an environment variable that sets the logging - // level of SDK helper/schema loggers. Infers root SDK logging level, if - // unset. - EnvTfLogSdkHelperSchema = "TF_LOG_SDK_HELPER_SCHEMA" ) diff --git a/internal/logging/helper_schema.go b/internal/logging/helper_schema.go deleted file mode 100644 index 0ecf6bf2e..000000000 --- a/internal/logging/helper_schema.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logging - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-log/tfsdklog" -) - -const ( - // SubsystemHelperSchema is the tfsdklog subsystem name for helper/schema. - SubsystemHelperSchema = "helper_schema" -) - -// HelperSchemaDebug emits a helper/schema subsystem log at DEBUG level. -func HelperSchemaDebug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemDebug(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaError emits a helper/schema subsystem log at ERROR level. -func HelperSchemaError(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemError(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaTrace emits a helper/schema subsystem log at TRACE level. -func HelperSchemaTrace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemTrace(ctx, SubsystemHelperSchema, msg, additionalFields...) -} - -// HelperSchemaWarn emits a helper/schema subsystem log at WARN level. -func HelperSchemaWarn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) { - tfsdklog.SubsystemWarn(ctx, SubsystemHelperSchema, msg, additionalFields...) -} diff --git a/internal/logging/helper_schema_test.go b/internal/logging/helper_schema_test.go deleted file mode 100644 index fea9fc7e3..000000000 --- a/internal/logging/helper_schema_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package logging_test - -import ( - "bytes" - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" - - "github.com/hashicorp/terraform-plugin-testing/internal/logging" -) - -func TestHelperSchemaDebug(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaDebug(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "debug", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaError(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaError(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "error", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaTrace(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaTrace(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "trace", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} - -func TestHelperSchemaWarn(t *testing.T) { - t.Parallel() - - var output bytes.Buffer - - ctx := tfsdklogtest.RootLogger(context.Background(), &output) - ctx = logging.InitContext(ctx) - - logging.HelperSchemaWarn(ctx, "test message") - - entries, err := tfsdklogtest.MultilineJSONDecode(&output) - - if err != nil { - t.Fatalf("unable to read multiple line JSON: %s", err) - } - - expectedEntries := []map[string]interface{}{ - { - "@level": "warn", - "@message": "test message", - "@module": "sdk.helper_schema", - }, - } - - if diff := cmp.Diff(entries, expectedEntries); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } -} diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 0cff33408..d29425c32 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -6,6 +6,7 @@ package plugintest import ( "context" "fmt" + "io" "os" "path/filepath" @@ -173,6 +174,40 @@ func (wd *WorkingDir) ClearState(ctx context.Context) error { return nil } +func (wd *WorkingDir) CopyState(ctx context.Context, src string) error { + srcState, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open statefile for read: %w", err) + } + + defer srcState.Close() + + dstState, err := os.Create(filepath.Join(wd.baseDir, "terraform.tfstate")) + if err != nil { + return fmt.Errorf("failed to open statefile for write: %w", err) + } + + defer dstState.Close() + + buf := make([]byte, 1024) + for { + n, err := srcState.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("failed to read from statefile: %w", err) + } + + _, err = dstState.Write(buf[:n]) + if err != nil { + return fmt.Errorf("failed to write to statefile: %w", err) + } + } + + return nil +} + // ClearPlan deletes any saved plan present in the working directory. func (wd *WorkingDir) ClearPlan(ctx context.Context) error { logging.HelperResourceTrace(ctx, "Clearing Terraform plan") @@ -295,6 +330,17 @@ func (wd *WorkingDir) HasSavedPlan() bool { return err == nil } +// RemoveResource removes a resource from state. +func (wd *WorkingDir) RemoveResource(ctx context.Context, address string) error { + logging.HelperResourceTrace(ctx, "Calling Terraform CLI state rm command") + + err := wd.tf.StateRm(context.Background(), address) + + logging.HelperResourceTrace(ctx, "Called Terraform CLI state rm command") + + return err +} + // SavedPlan returns an object describing the current saved plan file, if any. // // If no plan is saved or if the plan file cannot be read, SavedPlan returns @@ -349,6 +395,10 @@ func (wd *WorkingDir) State(ctx context.Context) (*tfjson.State, error) { return state, err } +func (wd *WorkingDir) StateFilePath() string { + return filepath.Join(wd.baseDir, "terraform.tfstate") +} + // Import runs terraform import func (wd *WorkingDir) Import(ctx context.Context, resource, id string) error { logging.HelperResourceTrace(ctx, "Calling Terraform CLI import command") diff --git a/internal/testing/testprovider/resource.go b/internal/testing/testprovider/resource.go index 8421e54d1..8969f5ca9 100644 --- a/internal/testing/testprovider/resource.go +++ b/internal/testing/testprovider/resource.go @@ -21,6 +21,7 @@ type Resource struct { PlanChangeFunc func(context.Context, resource.PlanChangeRequest, *resource.PlanChangeResponse) ReadResponse *resource.ReadResponse + IdentitySchemaResponse *resource.IdentitySchemaResponse SchemaResponse *resource.SchemaResponse UpdateResponse *resource.UpdateResponse UpgradeStateResponse *resource.UpgradeStateResponse @@ -31,6 +32,7 @@ func (r Resource) Create(ctx context.Context, req resource.CreateRequest, resp * if r.CreateResponse != nil { resp.Diagnostics = r.CreateResponse.Diagnostics resp.NewState = r.CreateResponse.NewState + resp.NewIdentity = r.CreateResponse.NewIdentity } } @@ -44,6 +46,7 @@ func (r Resource) ImportState(ctx context.Context, req resource.ImportStateReque if r.ImportStateResponse != nil { resp.Diagnostics = r.ImportStateResponse.Diagnostics resp.State = r.ImportStateResponse.State + resp.Identity = r.ImportStateResponse.Identity } } @@ -57,6 +60,14 @@ func (r Resource) Read(ctx context.Context, req resource.ReadRequest, resp *reso if r.ReadResponse != nil { resp.Diagnostics = r.ReadResponse.Diagnostics resp.NewState = r.ReadResponse.NewState + resp.NewIdentity = r.ReadResponse.NewIdentity + } +} + +func (r Resource) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if r.IdentitySchemaResponse != nil { + resp.Diagnostics = r.IdentitySchemaResponse.Diagnostics + resp.Schema = r.IdentitySchemaResponse.Schema } } @@ -71,6 +82,7 @@ func (r Resource) Update(ctx context.Context, req resource.UpdateRequest, resp * if r.UpdateResponse != nil { resp.Diagnostics = r.UpdateResponse.Diagnostics resp.NewState = r.UpdateResponse.NewState + resp.NewIdentity = r.UpdateResponse.NewIdentity } } diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 3c763914c..0855ff733 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -5,6 +5,7 @@ package providerserver import ( "context" + "errors" "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -49,11 +50,16 @@ func NewProviderServerWithError(p provider.Provider, err error) func() (tfprotov // By default, the following data is copied automatically: // // - ApplyResourceChange (create): req.Config -> resp.NewState +// - ApplyResourceChange (create): req.PlannedIdentity -> resp.NewIdentity // - ApplyResourceChange (delete): req.PlannedState -> resp.NewState // - ApplyResourceChange (update): req.PlannedState -> resp.NewState +// - ApplyResourceChange (update): req.PlannedIdentity -> resp.NewIdentity // - PlanResourceChange: req.ProposedNewState -> resp.PlannedState +// - PlanResourceChange: req.PriorIdentity -> resp.PlannedIdentity +// - ImportResourceState: req.Identity -> resp.ImportedResources[0].Identity // - ReadDataSource: req.Config -> resp.State // - ReadResource: req.CurrentState -> resp.NewState +// - ReadResource: req.CurrentIdentity -> resp.NewIdentity type ProviderServer struct { Provider provider.Provider } @@ -135,12 +141,40 @@ func (s ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6. return resp, nil } + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + var plannedIdentity *tftypes.Value + if identitySchemaResp.Schema != nil && req.PlannedIdentity != nil { + plannedIdentityVal, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.PlannedIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + plannedIdentity = &plannedIdentityVal + } + + var newIdentity *tftypes.Value if priorState.IsNull() { createReq := resource.CreateRequest{ - Config: config, + Config: config, + PlannedIdentity: plannedIdentity, } createResp := &resource.CreateResponse{ - NewState: config.Copy(), + NewState: config.Copy(), + NewIdentity: plannedIdentity, } r.Create(ctx, createReq, createResp) @@ -160,6 +194,7 @@ func (s ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6. } resp.NewState = newState + newIdentity = createResp.NewIdentity } else if plannedState.IsNull() { deleteReq := resource.DeleteRequest{ PriorState: priorState, @@ -177,12 +212,14 @@ func (s ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6. resp.NewState = req.PlannedState } else { updateReq := resource.UpdateRequest{ - Config: config, - PlannedState: plannedState, - PriorState: priorState, + Config: config, + PlannedState: plannedState, + PlannedIdentity: plannedIdentity, + PriorState: priorState, } updateResp := &resource.UpdateResponse{ - NewState: plannedState.Copy(), + NewState: plannedState.Copy(), + NewIdentity: plannedIdentity, } r.Update(ctx, updateReq, updateResp) @@ -202,6 +239,21 @@ func (s ProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6. } resp.NewState = newState + newIdentity = updateResp.NewIdentity + } + + if newIdentity != nil { + newIdentityVal, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *newIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: newIdentityVal, + } } return resp, nil @@ -286,6 +338,27 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge return resp, nil } +func (s ProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + resp := &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{}, + } + + for typeName, r := range s.Provider.ResourcesMap() { + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = append(resp.Diagnostics, identitySchemaResp.Diagnostics...) + + if identitySchemaResp.Schema != nil { + resp.IdentitySchemas[typeName] = identitySchemaResp.Schema + } + } + + return resp, nil +} + func (s ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { resp := &tfprotov6.ImportResourceStateResponse{} @@ -313,6 +386,31 @@ func (s ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6. } importResp := &resource.ImportStateResponse{} + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.Identity != nil { + identity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.Identity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + importReq.Identity = &identity + importResp.Identity = &identity + } + r.ImportState(ctx, importReq, importResp) resp.Diagnostics = importResp.Diagnostics @@ -347,6 +445,21 @@ func (s ProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6. }, } + if importResp.Identity != nil { + identity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *importResp.Identity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + // There is only one imported resource, so this should always be safe + resp.ImportedResources[0].Identity = &tfprotov6.ResourceIdentityData{ + IdentityData: identity, + } + } + return resp, nil } @@ -456,6 +569,31 @@ func (s ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.P PlannedState: proposedNewState.Copy(), } + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.PriorIdentity != nil { + priorIdentity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.PriorIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + planReq.PriorIdentity = &priorIdentity + planResp.PlannedIdentity = &priorIdentity + } + r.PlanChange(ctx, planReq, planResp) resp.Diagnostics = planResp.Diagnostics @@ -474,6 +612,20 @@ func (s ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.P return resp, nil } + if planResp.PlannedIdentity != nil { + plannedIdentity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *planResp.PlannedIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.PlannedIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: plannedIdentity, + } + } + resp.PlannedState = plannedState return resp, nil @@ -574,6 +726,31 @@ func (s ProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadRes NewState: currentState.Copy(), } + // Copy over identity if it's supported + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := &resource.IdentitySchemaResponse{} + + r.IdentitySchema(ctx, identitySchemaReq, identitySchemaResp) + + resp.Diagnostics = identitySchemaResp.Diagnostics + + if len(resp.Diagnostics) > 0 { + return resp, nil + } + + if identitySchemaResp.Schema != nil && req.CurrentIdentity != nil { + currentIdentity, diag := IdentityDynamicValueToValue(identitySchemaResp.Schema, req.CurrentIdentity.IdentityData) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + readReq.CurrentIdentity = ¤tIdentity + readResp.NewIdentity = ¤tIdentity + } + r.Read(ctx, readReq, readResp) resp.Diagnostics = readResp.Diagnostics @@ -592,6 +769,20 @@ func (s ProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadRes resp.NewState = newState + if readResp.NewIdentity != nil { + newIdentity, diag := IdentityValuetoDynamicValue(identitySchemaResp.Schema, *readResp.NewIdentity) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.NewIdentity = &tfprotov6.ResourceIdentityData{ + IdentityData: newIdentity, + } + } + return resp, nil } @@ -698,6 +889,11 @@ func (s ProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6 return resp, nil } +func (s ProviderServer) UpgradeResourceIdentity(context.Context, *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + // TODO: This isn't currently being used by the testing framework provider, so no need to implement it until then. + return nil, errors.New("UpgradeResourceIdentity is not currently implemented in testprovider") +} + func (s ProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { resp := &tfprotov6.ValidateDataResourceConfigResponse{} diff --git a/internal/testing/testsdk/providerserver/tftypes.go b/internal/testing/testsdk/providerserver/tftypes.go index 4b9e07ec7..a15ba1143 100644 --- a/internal/testing/testsdk/providerserver/tftypes.go +++ b/internal/testing/testsdk/providerserver/tftypes.go @@ -63,3 +63,59 @@ func ValuetoDynamicValue(schema *tfprotov6.Schema, value tftypes.Value) (*tfprot return &dynamicValue, nil } + +func IdentityDynamicValueToValue(schema *tfprotov6.ResourceIdentitySchema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing identity schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} + +func IdentityValuetoDynamicValue(schema *tfprotov6.ResourceIdentitySchema, value tftypes.Value) (*tfprotov6.DynamicValue, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: missing identity schema", + } + + return nil, diag + } + + dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), + } + + return &dynamicValue, diag + } + + return &dynamicValue, nil +} diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index 5fea34468..3fb3703ae 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -16,6 +16,7 @@ type Resource interface { ImportState(context.Context, ImportStateRequest, *ImportStateResponse) PlanChange(context.Context, PlanChangeRequest, *PlanChangeResponse) Read(context.Context, ReadRequest, *ReadResponse) + IdentitySchema(context.Context, IdentitySchemaRequest, *IdentitySchemaResponse) Schema(context.Context, SchemaRequest, *SchemaResponse) Update(context.Context, UpdateRequest, *UpdateResponse) UpgradeState(context.Context, UpgradeStateRequest, *UpgradeStateResponse) @@ -23,12 +24,14 @@ type Resource interface { } type CreateRequest struct { - Config tftypes.Value + Config tftypes.Value + PlannedIdentity *tftypes.Value } type CreateResponse struct { Diagnostics []*tfprotov6.Diagnostic NewState tftypes.Value + NewIdentity *tftypes.Value } type DeleteRequest struct { @@ -39,18 +42,28 @@ type DeleteResponse struct { Diagnostics []*tfprotov6.Diagnostic } +type IdentitySchemaRequest struct{} + +type IdentitySchemaResponse struct { + Diagnostics []*tfprotov6.Diagnostic + Schema *tfprotov6.ResourceIdentitySchema +} + type ImportStateRequest struct { - ID string + ID string + Identity *tftypes.Value } type ImportStateResponse struct { Diagnostics []*tfprotov6.Diagnostic State tftypes.Value + Identity *tftypes.Value } type PlanChangeRequest struct { Config tftypes.Value PriorState tftypes.Value + PriorIdentity *tftypes.Value ProposedNewState tftypes.Value } @@ -58,16 +71,19 @@ type PlanChangeResponse struct { Deferred *tfprotov6.Deferred Diagnostics []*tfprotov6.Diagnostic PlannedState tftypes.Value + PlannedIdentity *tftypes.Value RequiresReplace []*tftypes.AttributePath } type ReadRequest struct { - CurrentState tftypes.Value + CurrentState tftypes.Value + CurrentIdentity *tftypes.Value } type ReadResponse struct { Diagnostics []*tfprotov6.Diagnostic NewState tftypes.Value + NewIdentity *tftypes.Value } type SchemaRequest struct{} @@ -78,14 +94,16 @@ type SchemaResponse struct { } type UpdateRequest struct { - Config tftypes.Value - PlannedState tftypes.Value - PriorState tftypes.Value + Config tftypes.Value + PlannedState tftypes.Value + PlannedIdentity *tftypes.Value + PriorState tftypes.Value } type UpdateResponse struct { Diagnostics []*tfprotov6.Diagnostic NewState tftypes.Value + NewIdentity *tftypes.Value } type UpgradeStateRequest struct { diff --git a/internal/teststep/config.go b/internal/teststep/config.go index b81c264d9..91a708e26 100644 --- a/internal/teststep/config.go +++ b/internal/teststep/config.go @@ -45,6 +45,7 @@ type Config interface { HasProviderBlock(context.Context) (bool, error) HasTerraformBlock(context.Context) (bool, error) Write(context.Context, string) error + Append(string) Config } // PrepareConfigurationRequest is used to simplify the generation of @@ -151,7 +152,7 @@ func copyFiles(path string, dstPath string) error { if info.IsDir() { continue } else { - err = copyFile(srcPath, dstPath) + _, err = copyFile(srcPath, dstPath) if err != nil { return err @@ -164,11 +165,11 @@ func copyFiles(path string, dstPath string) error { // copyFile accepts a path to a file and a destination, // copying the file from path to destination. -func copyFile(path string, dstPath string) error { +func copyFile(path string, dstPath string) (string, error) { srcF, err := os.Open(path) if err != nil { - return err + return "", err } defer srcF.Close() @@ -176,7 +177,7 @@ func copyFile(path string, dstPath string) error { di, err := os.Stat(dstPath) if err != nil { - return err + return "", err } if di.IsDir() { @@ -187,12 +188,28 @@ func copyFile(path string, dstPath string) error { dstF, err := os.Create(dstPath) if err != nil { - return err + return "", err } defer dstF.Close() if _, err := io.Copy(dstF, srcF); err != nil { + return "", err + } + + return dstPath, nil +} + +// appendToFile accepts a path to a file and a string, +// appending the file from path to destination. +func appendToFile(path string, content string) error { + f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, content); err != nil { return err } diff --git a/internal/teststep/directory.go b/internal/teststep/directory.go index 0126e82aa..67ecc5ccd 100644 --- a/internal/teststep/directory.go +++ b/internal/teststep/directory.go @@ -5,6 +5,8 @@ package teststep import ( "context" + "fmt" + "hash/crc32" "os" "path/filepath" ) @@ -13,6 +15,9 @@ var _ Config = configurationDirectory{} type configurationDirectory struct { directory string + + // appendedConfig is a map of filenames to content + appendedConfig map[string]string } // HasConfigurationFiles is used during validation to ensure that @@ -85,10 +90,39 @@ func (c configurationDirectory) Write(ctx context.Context, dest string) error { } err := copyFiles(configDirectory, dest) + if err != nil { + return err + } + err = c.writeAppendedConfig(dest) if err != nil { return err } return nil } + +func (c configurationDirectory) Append(config string) Config { + if c.appendedConfig == nil { + c.appendedConfig = make(map[string]string) + } + + checksum := crc32.Checksum([]byte(config), crc32.IEEETable) + filename := fmt.Sprintf("terraform_plugin_test_%d.tf", checksum) + + c.appendedConfig[filename] = config + return c +} + +func (c configurationDirectory) writeAppendedConfig(dstPath string) error { + for filename, config := range c.appendedConfig { + outFilename := filepath.Join(dstPath, filename) + + err := os.WriteFile(outFilename, []byte(config), 0700) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/teststep/directory_test.go b/internal/teststep/directory_test.go index 0118b91d5..cb88d214f 100644 --- a/internal/teststep/directory_test.go +++ b/internal/teststep/directory_test.go @@ -432,17 +432,17 @@ func TestConfigurationDirectory_Write(t *testing.T) { }, "no-config": { configDirectory: configurationDirectory{ - "testdata/empty_dir", + directory: "testdata/empty_dir", }, }, "dir-single-file": { configDirectory: configurationDirectory{ - "testdata/random", + directory: "testdata/random", }, }, "dir-multiple-files": { configDirectory: configurationDirectory{ - "testdata/random_multiple_files", + directory: "testdata/random_multiple_files", }, }, } @@ -523,17 +523,17 @@ func TestConfigurationDirectory_Write_AbsolutePath(t *testing.T) { }, "no-config": { configDirectory: configurationDirectory{ - "testdata/empty_dir", + directory: "testdata/empty_dir", }, }, "dir-single-file": { configDirectory: configurationDirectory{ - "testdata/random", + directory: "testdata/random", }, }, "dir-multiple-files": { configDirectory: configurationDirectory{ - "testdata/random_multiple_files", + directory: "testdata/random_multiple_files", }, }, } @@ -607,6 +607,81 @@ func TestConfigurationDirectory_Write_AbsolutePath(t *testing.T) { } } +func TestConfigurationDirectory_Write_WithAppendedConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + configDirectory configurationDirectory + expectedError *regexp.Regexp + }{ + "dir-single-file": { + configDirectory: configurationDirectory{ + directory: "testdata/random", + appendedConfig: map[string]string{ + "import.tf": `terraform {\nimport\n{\nto = satellite.the_moon\nid = "moon"\n}\n}\n`, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + err := testCase.configDirectory.Write(context.Background(), tempDir) + if err != nil { + t.Errorf("unexpected error %s", err) + } + + dirEntries, err := os.ReadDir(testCase.configDirectory.directory) + if err != nil { + t.Errorf("error reading directory: %s", err) + } + + tempDirEntries, err := os.ReadDir(tempDir) + + if err != nil { + t.Errorf("error reading temp directory: %s", err) + } + + if len(tempDirEntries)-len(dirEntries) != 1 { + t.Errorf("expected %d dir entries, got %d dir entries", len(dirEntries)+1, tempDirEntries) + } + + for _, entry := range dirEntries { + filename := entry.Name() + expectedContent, err := os.ReadFile(filepath.Join(testCase.configDirectory.directory, filename)) + if err != nil { + t.Errorf("error reading file from config directory %s: %s", filename, err) + } + + content, err := os.ReadFile(filepath.Join(tempDir, filename)) + if err != nil { + t.Errorf("error reading generated file %s: %s", filename, err) + } + + if diff := cmp.Diff(expectedContent, content); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + + appendedConfigFiles := testCase.configDirectory.appendedConfig + for filename, expectedContent := range appendedConfigFiles { + content, err := os.ReadFile(filepath.Join(tempDir, filename)) + if err != nil { + t.Errorf("error reading appended config file %s: %s", filename, err) + } + + if diff := cmp.Diff([]byte(expectedContent), content); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + } + }) + } +} + var fileInfoComparer = cmp.Comparer(func(x, y os.FileInfo) bool { if x.Name() != y.Name() { return false diff --git a/internal/teststep/file.go b/internal/teststep/file.go index 6de3f0752..75ee6f7d6 100644 --- a/internal/teststep/file.go +++ b/internal/teststep/file.go @@ -12,7 +12,8 @@ import ( var _ Config = configurationFile{} type configurationFile struct { - file string + file string + appendedConfig string } // HasConfigurationFiles is used during validation to ensure that @@ -84,11 +85,24 @@ func (c configurationFile) Write(ctx context.Context, dest string) error { configFile = filepath.Join(pwd, configFile) } - err := copyFile(configFile, dest) - + destPath, err := copyFile(configFile, dest) if err != nil { return err } + if len(c.appendedConfig) > 0 { + err := appendToFile(destPath, c.appendedConfig) + if err != nil { + return err + } + } + return nil } + +func (c configurationFile) Append(config string) Config { + return configurationFile{ + file: c.file, + appendedConfig: config, + } +} diff --git a/internal/teststep/file_test.go b/internal/teststep/file_test.go index 5d0ebb73f..b6f89bed6 100644 --- a/internal/teststep/file_test.go +++ b/internal/teststep/file_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-testing/config" ) func TestConfigurationFile_HasProviderBlock(t *testing.T) { @@ -432,7 +433,7 @@ func TestConfigurationFile_Write(t *testing.T) { }, "file": { configFile: configurationFile{ - "testdata/random/random.tf", + file: "testdata/random/random.tf", }, }, } @@ -495,7 +496,7 @@ func TestConfigurationFile_Write_AbsolutePath(t *testing.T) { }, "file": { configFile: configurationFile{ - "testdata/random/random.tf", + file: "testdata/random/random.tf", }, }, } @@ -550,3 +551,49 @@ func TestConfigurationFile_Write_AbsolutePath(t *testing.T) { }) } } + +func TestConfigFile_Append(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + filename string + appendContent string + expectedContent string + }{ + "append content to a ConfigFile": { + filename: `testdata/main.tf`, // Contains `// Hello world` + appendContent: `terraform {}`, + expectedContent: "# Copyright (c) HashiCorp, Inc.\n# SPDX-License-Identifier: MPL-2.0\n\n// Hello world" + "\n" + "terraform {}", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + prepareConfigRequest := PrepareConfigurationRequest{ + File: func(config.TestStepConfigRequest) string { + return testCase.filename + }, + } + + teststepConfig := Configuration(prepareConfigRequest.Exec()) + teststepConfig = teststepConfig.Append(testCase.appendContent) + + tempdir := t.TempDir() + if err := teststepConfig.Write(context.Background(), tempdir); err != nil { + t.Fatalf("failed to write file: %s", err) + } + + got, err := os.ReadFile(filepath.Join(tempdir, filepath.Base(testCase.filename))) + if err != nil { + t.Fatalf("failed to read file: %s", err) + } + + gotS := string(got[:]) + if diff := cmp.Diff(testCase.expectedContent, gotS); diff != "" { + t.Errorf("expected %+v, got %+v", testCase.expectedContent, gotS) + } + }) + } +} diff --git a/internal/teststep/string.go b/internal/teststep/string.go index 4143b484d..39028682a 100644 --- a/internal/teststep/string.go +++ b/internal/teststep/string.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "strings" ) var _ Config = configurationString{} @@ -59,3 +60,9 @@ func (c configurationString) Write(ctx context.Context, dest string) error { return nil } + +func (c configurationString) Append(config string) Config { + return configurationString{ + raw: strings.Join([]string{c.raw, config}, "\n"), + } +} diff --git a/internal/teststep/testdata/main.tf b/internal/teststep/testdata/main.tf new file mode 100644 index 000000000..ba356816b --- /dev/null +++ b/internal/teststep/testdata/main.tf @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +// Hello world diff --git a/knownvalue/object.go b/knownvalue/object.go index e15c03928..cc97542c4 100644 --- a/knownvalue/object.go +++ b/knownvalue/object.go @@ -106,7 +106,7 @@ func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPr for i, k := range deltaKeys { if i == 0 { deltaMsg += msgPrefix - } else if i != 0 { + } else { deltaMsg += ", " } deltaMsg += fmt.Sprintf("%q", k) diff --git a/statecheck/expect_identity.go b/statecheck/expect_identity.go new file mode 100644 index 000000000..df5147b23 --- /dev/null +++ b/statecheck/expect_identity.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "maps" + "slices" + "sort" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" +) + +var _ StateCheck = expectIdentity{} + +type expectIdentity struct { + resourceAddress string + identity map[string]knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectIdentity) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + if len(resource.IdentityValues) != len(e.identity) { + deltaMsg := "" + if len(resource.IdentityValues) > len(e.identity) { + deltaMsg = createDeltaString(resource.IdentityValues, e.identity, "actual identity has extra attribute(s): ") + } else { + deltaMsg = createDeltaString(e.identity, resource.IdentityValues, "actual identity is missing attribute(s): ") + } + + resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.resourceAddress, len(e.identity), len(resource.IdentityValues), deltaMsg) + return + } + + var keys []string + + for k := range e.identity { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, k := range keys { + actualIdentityVal, ok := resource.IdentityValues[k] + + if !ok { + resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.resourceAddress, k) + return + } + + if err := e.identity[k].CheckValue(actualIdentityVal); err != nil { + resp.Error = fmt.Errorf("%s - %q identity attribute: %s", e.resourceAddress, k, err) + return + } + } +} + +// ExpectIdentity returns a state check that asserts that the identity at the given resource matches a known object, where each +// map key represents an identity attribute name. The identity in state must exactly match the given object and any missing/extra +// attributes will raise a diagnostic. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentity(resourceAddress string, identity map[string]knownvalue.Check) StateCheck { + return expectIdentity{ + resourceAddress: resourceAddress, + identity: identity, + } +} + +// createDeltaString prints the map keys that are present in mapA and not present in mapB +func createDeltaString[T any, V any](mapA map[string]T, mapB map[string]V, msgPrefix string) string { + deltaMsg := "" + + deltaMap := make(map[string]T, len(mapA)) + maps.Copy(deltaMap, mapA) + for key := range mapB { + delete(deltaMap, key) + } + + deltaKeys := slices.Sorted(maps.Keys(deltaMap)) + + for i, k := range deltaKeys { + if i == 0 { + deltaMsg += msgPrefix + } else { + deltaMsg += ", " + } + deltaMsg += fmt.Sprintf("%q", k) + } + + return deltaMsg +} diff --git a/statecheck/expect_identity_example_test.go b/statecheck/expect_identity_example_test.go new file mode 100644 index 000000000..1db3766c4 --- /dev/null +++ b/statecheck/expect_identity_example_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentity() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with "id" and "name" string attributes + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "test_resource.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "name": knownvalue.StringExact("John Doe"), + }, + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_test.go b/statecheck/expect_identity_test.go new file mode 100644 index 000000000..20032bfe0 --- /dev/null +++ b/statecheck/expect_identity_test.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentity_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.two", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentity_CheckState(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.Bool(true), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected bool value for Bool check, got: string`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - "id" identity attribute: expected value 321-id for StringExact check, got: id-123`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_ExtraAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("321-id"), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 1 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity has extra attribute\(s\): "list_of_numbers"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MissingAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "id": knownvalue.StringExact("id-123"), + "nonexistent_attr": knownvalue.StringExact("hello"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Expected 3 attribute\(s\) in the actual identity object, got 2 attribute\(s\): actual identity is missing attribute\(s\): "nonexistent_attr"`), + }, + }, + }) +} + +func TestExpectIdentity_CheckState_MismatchedAttribute(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity( + "examplecloud_thing.one", + map[string]knownvalue.Check{ + "not_id": knownvalue.StringExact("id-123"), + "list_of_numbers": knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.Int64Exact(1), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(4), + }, + ), + }, + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - missing attribute "not_id" in actual identity object`), + }, + }, + }) +} diff --git a/statecheck/expect_identity_value.go b/statecheck/expect_identity_value.go new file mode 100644 index 000000000..22da58ea8 --- /dev/null +++ b/statecheck/expect_identity_value.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValue{} + +type expectIdentityValue struct { + resourceAddress string + attributePath tfjsonpath.Path + identityValue knownvalue.Check +} + +// CheckState implements the state check logic. +func (e expectIdentityValue) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + result, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if err := e.identityValue.CheckValue(result); err != nil { + resp.Error = fmt.Errorf("error checking identity value for attribute at path: %s.%s, err: %s", e.resourceAddress, e.attributePath.String(), err) + + return + } +} + +// ExpectIdentityValue returns a state check that asserts that the specified identity attribute at the given resource +// matches a known value. This state check can only be used with managed resources that support resource identity. +// +// Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValue(resourceAddress string, attributePath tfjsonpath.Path, identityValue knownvalue.Check) StateCheck { + return expectIdentityValue{ + resourceAddress: resourceAddress, + attributePath: attributePath, + identityValue: identityValue, + } +} diff --git a/statecheck/expect_identity_value_example_test.go b/statecheck/expect_identity_value_example_test.go new file mode 100644 index 000000000..38aa506f2 --- /dev/null +++ b/statecheck/expect_identity_value_example_test.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValue() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource" has an identity schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "test_resource.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state.go b/statecheck/expect_identity_value_matches_state.go new file mode 100644 index 000000000..1e3c6ea14 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValueMatchesState{} + +type expectIdentityValueMatchesState struct { + resourceAddress string + attributePath tfjsonpath.Path +} + +// CheckState implements the state check logic. +func (e expectIdentityValueMatchesState) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.attributePath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, stateResult) { + resp.Error = fmt.Errorf("expected identity and state value at path to match, but they differ: %s.%s, identity value: %v, state value: %v", e.resourceAddress, e.attributePath.String(), identityResult, stateResult) + + return + } +} + +// ExpectIdentityValueMatchesState returns a state check that asserts that the specified identity attribute at the given resource +// matches the same attribute in state. This is useful when an identity attribute is in sync with a state attribute of the same path. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesState(resourceAddress string, attributePath tfjsonpath.Path) StateCheck { + return expectIdentityValueMatchesState{ + resourceAddress: resourceAddress, + attributePath: attributePath, + } +} diff --git a/statecheck/expect_identity_value_matches_state_at_path.go b/statecheck/expect_identity_value_matches_state_at_path.go new file mode 100644 index 000000000..257243998 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck + +import ( + "context" + "fmt" + "reflect" + + tfjson "github.com/hashicorp/terraform-json" + + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +var _ StateCheck = expectIdentityValueMatchesStateAtPath{} + +type expectIdentityValueMatchesStateAtPath struct { + resourceAddress string + identityAttrPath tfjsonpath.Path + stateAttrPath tfjsonpath.Path +} + +// CheckState implements the state check logic. +func (e expectIdentityValueMatchesStateAtPath) CheckState(ctx context.Context, req CheckStateRequest, resp *CheckStateResponse) { + var resource *tfjson.StateResource + + if req.State == nil { + resp.Error = fmt.Errorf("state is nil") + + return + } + + if req.State.Values == nil { + resp.Error = fmt.Errorf("state does not contain any state values") + + return + } + + if req.State.Values.RootModule == nil { + resp.Error = fmt.Errorf("state does not contain a root module") + + return + } + + for _, r := range req.State.Values.RootModule.Resources { + if e.resourceAddress == r.Address { + resource = r + + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("%s - Resource not found in state", e.resourceAddress) + + return + } + + if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 { + resp.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", e.resourceAddress) + + return + } + + identityResult, err := tfjsonpath.Traverse(resource.IdentityValues, e.identityAttrPath) + + if err != nil { + resp.Error = err + + return + } + + stateResult, err := tfjsonpath.Traverse(resource.AttributeValues, e.stateAttrPath) + + if err != nil { + resp.Error = err + + return + } + + if !reflect.DeepEqual(identityResult, stateResult) { + resp.Error = fmt.Errorf( + "expected identity (%[1]s.%[2]s) and state value (%[1]s.%[3]s) to match, but they differ: identity value: %[4]v, state value: %[5]v", + e.resourceAddress, + e.identityAttrPath.String(), + e.stateAttrPath.String(), + identityResult, + stateResult, + ) + + return + } +} + +// ExpectIdentityValueMatchesStateAtPath returns a state check that asserts that the specified identity attribute at the given resource +// matches the specified attribute in state. This is useful when an identity attribute is in sync with a state attribute of a different path. +// +// This state check can only be used with managed resources that support resource identity. Resource identity is only supported in Terraform v1.12+ +func ExpectIdentityValueMatchesStateAtPath(resourceAddress string, identityAttrPath, stateAttrPath tfjsonpath.Path) StateCheck { + return expectIdentityValueMatchesStateAtPath{ + resourceAddress: resourceAddress, + identityAttrPath: identityAttrPath, + stateAttrPath: stateAttrPath, + } +} diff --git a/statecheck/expect_identity_value_matches_state_at_path_example_test.go b/statecheck/expect_identity_value_matches_state_at_path_example_test.go new file mode 100644 index 000000000..474864631 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesStateAtPath() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "identity_id" string attribute + // - Has a resource schema with an "state_id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // The identity attribute at "identity_id" and state attribute at "state_id" must match + statecheck.ExpectIdentityValueMatchesStateAtPath( + "test_resource.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("state_id"), + ), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_at_path_test.go b/statecheck/expect_identity_value_matches_state_at_path_test.go new file mode 100644 index 000000000..3ee7a4a64 --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_at_path_test.go @@ -0,0 +1,344 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.two", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_id"), + tfjsonpath.New("state_id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("id"), + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.id\) and state value \(examplecloud_thing.one.id\) to match, but they differ: identity value: id-123, state value: 321-di`), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentityDifferentPaths(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("identity_list_of_numbers"), + tfjsonpath.New("state_list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesStateAtPath_CheckState_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesStateAtPath( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity \(examplecloud_thing.one.list_of_numbers\) and state value \(examplecloud_thing.one.list_of_numbers\) to match, but they differ: identity value: \[1 2 3 4\], state value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentityDifferentPaths() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "state_id": tftypes.String, + "state_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "state_id": tftypes.NewValue(tftypes.String, "id-123"), + "state_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "state_id": tftypes.String, + "state_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "state_id": tftypes.NewValue(tftypes.String, "id-123"), + "state_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "identity_id": tftypes.String, + "identity_list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "identity_id": tftypes.NewValue(tftypes.String, "id-123"), + "identity_list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "identity_id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "identity_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "state_id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "state_list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_example_test.go b/statecheck/expect_identity_value_matches_state_example_test.go new file mode 100644 index 000000000..df0dd546c --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_example_test.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func ExampleExpectIdentityValueMatchesState() { + // A typical test would accept *testing.T as a function parameter, for instance `func TestSomething(t *testing.T) { ... }`. + t := &testing.T{} + t.Parallel() + + resource.Test(t, resource.TestCase{ + // Resource identity support is only available in Terraform v1.12+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + // Provider definition omitted. Assuming "test_resource": + // - Has an identity schema with an "id" string attribute + // - Has a resource schema with an "id" string attribute + Steps: []resource.TestStep{ + { + Config: `resource "test_resource" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + // The identity attribute and state attribute at "id" must match + statecheck.ExpectIdentityValueMatchesState("test_resource.one", tfjsonpath.New("id")), + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_matches_state_test.go b/statecheck/expect_identity_value_matches_state_test.go new file mode 100644 index 000000000..d3248e15b --- /dev/null +++ b/statecheck/expect_identity_value_matches_state_test.go @@ -0,0 +1,337 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValueMatchesState_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.two", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_String_Matches(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_String_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("id"), + ), + }, + ExpectError: regexp.MustCompile("expected identity and state value at path to match, but they differ: examplecloud_thing.one.id, identity value: id-123, state value: 321-di"), + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValueMatchesState_CheckState_List_DoesntMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithMismatchedResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValueMatchesState( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + ), + }, + ExpectError: regexp.MustCompile(`expected identity and state value at path to match, but they differ: examplecloud_thing.one.list_of_numbers, identity value: \[1 2 3 4\], state value: \[4 3 2 1\]`), + }, + }, + }) +} + +func examplecloudProviderWithMismatchedResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "321-di"), // doesn't match identity -> id + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 4), // doesn't match identity -> list_of_numbers[0] + tftypes.NewValue(tftypes.Number, 3), // doesn't match identity -> list_of_numbers[1] + tftypes.NewValue(tftypes.Number, 2), // doesn't match identity -> list_of_numbers[2] + tftypes.NewValue(tftypes.Number, 1), // doesn't match identity -> list_of_numbers[3] + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/statecheck/expect_identity_value_test.go b/statecheck/expect_identity_value_test.go new file mode 100644 index 000000000..8ee701272 --- /dev/null +++ b/statecheck/expect_identity_value_test.go @@ -0,0 +1,461 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package statecheck_test + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/teststep" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestExpectIdentityValue_CheckState_ResourceNotFound(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.two", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile("examplecloud_thing.two - Resource not found in state"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_No_Terraform_Identity_Support(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_0_0), // ProtoV6ProviderFactories + tfversion.SkipAbove(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource support identity, but the Terraform versions running will not. + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_No_Identity(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + // Resource does not support identity + "examplecloud": examplecloudProviderNoIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123"), + ), + }, + ExpectError: regexp.MustCompile(`examplecloud_thing.one - Identity not found in state. Either the resource ` + + `does not support identity or the Terraform version running the test does not support identity. \(must be v1.12\+\)`, + ), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("id-123")), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.Bool(true)), + }, + ExpectError: regexp.MustCompile("expected bool value for Bool check, got: string"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_String_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("id"), + knownvalue.StringExact("321-id")), + }, + ExpectError: regexp.MustCompile("expected value 321-id for StringExact check, got: id-123"), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(0), + knownvalue.Int64Exact(1), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(1), + knownvalue.Int64Exact(2), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(2), + knownvalue.Int64Exact(3), + ), + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers").AtSliceIndex(3), + knownvalue.Int64Exact(4), + ), + }, + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List_KnownValueWrongType(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {} + `, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.MapExact(map[string]knownvalue.Check{}), + ), + }, + ExpectError: regexp.MustCompile(`expected map\[string\]any value for MapExact check, got: \[\]interface {}`), + }, + }, + }) +} + +func TestExpectIdentityValue_CheckState_List_KnownValueWrongValue(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "examplecloud": examplecloudProviderWithResourceIdentity(), + }, + Steps: []r.TestStep{ + { + Config: `resource "examplecloud_thing" "one" {}`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentityValue( + "examplecloud_thing.one", + tfjsonpath.New("list_of_numbers"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.Int64Exact(4), + knownvalue.Int64Exact(3), + knownvalue.Int64Exact(2), + knownvalue.Int64Exact(1), + }), + ), + }, + ExpectError: regexp.MustCompile(`list element index 0: expected value 4 for Int64Exact check, got: 1`), + }, + }, + }) +} + +func examplecloudProviderWithResourceIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + ), + NewIdentity: teststep.Pointer(tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list_of_numbers": tftypes.List{ElementType: tftypes.Number}, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "list_of_numbers": tftypes.NewValue( + tftypes.List{ElementType: tftypes.Number}, + []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 1), + tftypes.NewValue(tftypes.Number, 2), + tftypes.NewValue(tftypes.Number, 3), + tftypes.NewValue(tftypes.Number, 4), + }, + ), + }, + )), + }, + IdentitySchemaResponse: &resource.IdentitySchemaResponse{ + Schema: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + RequiredForImport: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + OptionalForImport: true, + }, + }, + }, + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "list_of_numbers", + Type: tftypes.List{ElementType: tftypes.Number}, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} + +func examplecloudProviderNoIdentity() func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "examplecloud_thing": { + CreateResponse: &resource.CreateResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + ReadResponse: &resource.ReadResponse{ + NewState: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "test value"), + }, + ), + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) +} diff --git a/tfversion/versions.go b/tfversion/versions.go index ac734e598..ffb625c8d 100644 --- a/tfversion/versions.go +++ b/tfversion/versions.go @@ -38,4 +38,5 @@ var ( Version1_9_0 *version.Version = version.Must(version.NewVersion("1.9.0")) Version1_10_0 *version.Version = version.Must(version.NewVersion("1.10.0")) Version1_11_0 *version.Version = version.Must(version.NewVersion("1.11.0")) + Version1_12_0 *version.Version = version.Must(version.NewVersion("1.12.0")) ) diff --git a/tools/go.mod b/tools/go.mod index 938dd04b7..b9c4f8c01 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -2,7 +2,7 @@ module tools go 1.23.7 -require github.com/hashicorp/copywrite v0.21.0 +require github.com/hashicorp/copywrite v0.22.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 // indirect @@ -17,7 +17,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/golang-jwt/jwt/v4 v4.5.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v45 v45.2.0 // indirect github.com/google/go-github/v53 v53.0.0 // indirect @@ -49,7 +49,7 @@ require ( go.mongodb.org/mongo-driver v1.10.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index abe7c9dc7..ac09f376c 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -96,8 +96,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -144,8 +144,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/copywrite v0.21.0 h1:IE8uByQdos8s0uAyHF4O8RHV5cJEhmIc+Awk+wkKXKI= -github.com/hashicorp/copywrite v0.21.0/go.mod h1:mu6DAyUI6m6vq8weoJn9a0HDuUUrV+0GQdRp4mD50yU= +github.com/hashicorp/copywrite v0.22.0 h1:mqjMrgP3VptS7aLbu2l39rtznoK+BhphHst6i7HiTAo= +github.com/hashicorp/copywrite v0.22.0/go.mod h1:FqvGJt2+yoYDpVYgFSdg3R2iyhkCVaBmPMhfso0MR2k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -411,8 +411,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=