diff --git a/.changes/1.14.0.md b/.changes/1.14.0.md new file mode 100644 index 000000000..7d364f3b8 --- /dev/null +++ b/.changes/1.14.0.md @@ -0,0 +1,16 @@ +## 1.14.0 (February 19, 2025) + +NOTES: + +* Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. ([#1044](https://github.com/hashicorp/terraform-plugin-framework/issues/1044)) +* ephemeral: Ephemeral resources are now considered generally available and protected by compatibility promises. ([#1052](https://github.com/hashicorp/terraform-plugin-framework/issues/1052)) + +FEATURES: + +* resource/schema: Added `WriteOnly` schema field for managed resource schemas to indicate a write-only attribute. Write-only attribute values are not saved to the Terraform plan or state artifacts. ([#1044](https://github.com/hashicorp/terraform-plugin-framework/issues/1044)) + +BUG FIXES: + +* internal/fwschemadata: Set semantic equality logic has been adjusted and will now ignore order of elements during comparison. ([#1061](https://github.com/hashicorp/terraform-plugin-framework/issues/1061)) +* internal/fwserver: Fixed bug where dynamic attributes would not prompt invalid configuration error messages ([#1090](https://github.com/hashicorp/terraform-plugin-framework/issues/1090)) + diff --git a/.changes/1.14.1.md b/.changes/1.14.1.md new file mode 100644 index 000000000..4d9041db4 --- /dev/null +++ b/.changes/1.14.1.md @@ -0,0 +1,6 @@ +## 1.14.1 (February 20, 2025) + +BUG FIXES: + +* internal/fwserver: fixed bug where write-only attributes set in configuration would cause perpetual diffs for computed attributes. ([#1097](https://github.com/hashicorp/terraform-plugin-framework/issues/1097)) + diff --git a/.changes/1.15.0-alpha.1.md b/.changes/1.15.0-alpha.1.md new file mode 100644 index 000000000..0c6f4f9cf --- /dev/null +++ b/.changes/1.15.0-alpha.1.md @@ -0,0 +1,7 @@ +## 1.15.0-alpha.1 (March 18, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1114](https://github.com/hashicorp/terraform-plugin-framework/issues/1114)) +* This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250312, to store and read identity data during plan and apply workflows. A managed resource identity can be used by implementing the optional `resource.ResourceWithIdentity` interface and defining an identity schema. Once the identity schema is defined, you can read and store identity data in the state file via the new `Identity` fields in the response objects on the resource CRUD methods. ([#1112](https://github.com/hashicorp/terraform-plugin-framework/issues/1112)) + diff --git a/.changes/1.15.0-beta.1.md b/.changes/1.15.0-beta.1.md new file mode 100644 index 000000000..a4f06e93c --- /dev/null +++ b/.changes/1.15.0-beta.1.md @@ -0,0 +1,6 @@ +## 1.15.0-beta.1 (April 15, 2025) + +NOTES: + +* This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta1. Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the `resource.ResourceWithImportState` interface will automatically pass-through identity data to the `Read` method. The `RequiredForImport` and `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. ([#1126](https://github.com/hashicorp/terraform-plugin-framework/issues/1126)) + diff --git a/.changes/unreleased/upcoming-stable/NOTES-20250317-153311.yaml b/.changes/unreleased/upcoming-stable/NOTES-20250317-153311.yaml new file mode 100644 index 000000000..032cbf254 --- /dev/null +++ b/.changes/unreleased/upcoming-stable/NOTES-20250317-153311.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: 'all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). + It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) + before upgrading. Any consumers building on earlier Go versions may experience errors.' +time: 2025-03-17T15:33:11.058742-04:00 +custom: + Issue: "1114" diff --git a/.copywrite.hcl b/.copywrite.hcl index 12b18fcf4..0494a4fe5 100644 --- a/.copywrite.hcl +++ b/.copywrite.hcl @@ -6,7 +6,7 @@ project { header_ignore = [ # 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 5d5ed18c4..36958c1cc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,17 @@ * @hashicorp/terraform-devex -# web presence and education +# 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/data/ +/website/public/ +/website/content/ -/website/ @hashicorp/team-docs-packer-and-terraform @hashicorp/web-presence @hashicorp/terraform-devex +# 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 diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 53578c4a2..0773b8bc9 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@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.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 0721580cf..85e8df51e 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@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 + - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 terraform-provider-corner-tfprotov5: defaults: run: @@ -34,7 +34,7 @@ jobs: with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 @@ -60,7 +60,7 @@ jobs: with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 @@ -79,16 +79,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.23', '1.22' ] + go-version: [ '1.24', '1.23' ] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ matrix.go-version }} - run: go mod download - run: go test -coverprofile=coverage.out ./... - run: go tool cover -html=coverage.out -o coverage.html - - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: go-${{ matrix.go-version }}-coverage path: coverage.html diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index e4181d55f..59a7b3d8f 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@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da0415909..f976e4efe 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@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0 + - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.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@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 + - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.golangci.yml b/.golangci.yml index 429044d2c..1515f53ae 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,16 +1,12 @@ -issues: - max-per-linter: 0 - max-same-issues: 0 - +version: "2" linters: - disable-all: true + default: none enable: + - copyloopvar - durationcheck - errcheck - - exportloopref - forcetypeassert - - gofmt - - gosimple + - govet - ineffassign - makezero - misspell @@ -18,12 +14,38 @@ linters: - paralleltest - predeclared - staticcheck - - tenv - unconvert - unparam - unused - - govet - -run: - # Prevent false positive timeouts in CI - timeout: 5m + - usetesting + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + settings: + staticcheck: + checks: + - all + - '-QF1008' # could remove embedded field from selector -- https://staticcheck.io/docs/checks#QF1008 + - '-ST1003' # struct field Id should be ID -- https://staticcheck.io/docs/checks#ST1003 + - '-ST1005' # error strings should not be capitalized -- https://staticcheck.io/docs/checks#ST1005 + - '-ST1016' # methods on the same type should have the same receiver name -- https://staticcheck.io/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 e8f5fee93..78d877e55 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 67f78f7fd..69ca27d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +## 1.15.0-beta.1 (April 15, 2025) + +NOTES: + +* This beta pre-release continues the implementation of managed resource identity, which should now be used with Terraform v1.12.0-beta1. Managed resources now can support import by identity during plan and apply workflows. Managed resources that already support import via the `resource.ResourceWithImportState` interface will automatically pass-through identity data to the `Read` method. The `RequiredForImport` and `OptionalForImport` fields on the identity schema can be used to control the validation that Terraform core will apply to the import config block. ([#1126](https://github.com/hashicorp/terraform-plugin-framework/issues/1126)) + +## 1.15.0-alpha.1 (March 18, 2025) + +NOTES: + +* all: This Go module has been updated to Go 1.23 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.23 release notes](https://go.dev/doc/go1.23) before upgrading. Any consumers building on earlier Go versions may experience errors. ([#1114](https://github.com/hashicorp/terraform-plugin-framework/issues/1114)) +* This alpha pre-release contains an initial implementation for managed resource identity, which can used with Terraform v1.12.0-alpha20250312, to store and read identity data during plan and apply workflows. A managed resource identity can be used by implementing the optional `resource.ResourceWithIdentity` interface and defining an identity schema. Once the identity schema is defined, you can read and store identity data in the state file via the new `Identity` fields in the response objects on the resource CRUD methods. ([#1112](https://github.com/hashicorp/terraform-plugin-framework/issues/1112)) + +## 1.14.1 (February 20, 2025) + +BUG FIXES: + +* internal/fwserver: fixed bug where write-only attributes set in configuration would cause perpetual diffs for computed attributes. ([#1097](https://github.com/hashicorp/terraform-plugin-framework/issues/1097)) + +## 1.14.0 (February 19, 2025) + +NOTES: + +* Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. ([#1044](https://github.com/hashicorp/terraform-plugin-framework/issues/1044)) +* ephemeral: Ephemeral resources are now considered generally available and protected by compatibility promises. ([#1052](https://github.com/hashicorp/terraform-plugin-framework/issues/1052)) + +FEATURES: + +* resource/schema: Added `WriteOnly` schema field for managed resource schemas to indicate a write-only attribute. Write-only attribute values are not saved to the Terraform plan or state artifacts. ([#1044](https://github.com/hashicorp/terraform-plugin-framework/issues/1044)) + +BUG FIXES: + +* internal/fwschemadata: Set semantic equality logic has been adjusted and will now ignore order of elements during comparison. ([#1061](https://github.com/hashicorp/terraform-plugin-framework/issues/1061)) +* internal/fwserver: Fixed bug where dynamic attributes would not prompt invalid configuration error messages ([#1090](https://github.com/hashicorp/terraform-plugin-framework/issues/1090)) + ## 1.13.0 (October 31, 2024) NOTES: diff --git a/README.md b/README.md index 294ba5ead..0b2041e25 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Providers built with this framework are compatible with Terraform version v0.12 This project follows the [support policy](https://golang.org/doc/devel/release.html#policy) of Go as its support policy. The two latest major releases of Go are supported by the project. -Currently, that means Go **1.22** or later must be used when including this project as a dependency. +Currently, that means Go **1.23** or later must be used when including this project as a dependency. ## Contributing diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index b9f6e3820..5830d6c29 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,7 +182,24 @@ func (a BoolAttribute) IsRequired() bool { return a.Required } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + // IsSensitive returns the Sensitive field value. func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/bool_attribute_test.go b/datasource/schema/bool_attribute_test.go index 40e36a08d..e2fa2d2a6 100644 --- a/datasource/schema/bool_attribute_test.go +++ b/datasource/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestBoolAttributeBoolValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestBoolAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestBoolAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestBoolAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestBoolAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestBoolAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestBoolAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestBoolAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestBoolAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 6b1b6c83e..2d489fe83 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -182,6 +183,23 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/datasource/schema/dynamic_attribute_test.go b/datasource/schema/dynamic_attribute_test.go index 8981dbecd..05cd9e340 100644 --- a/datasource/schema/dynamic_attribute_test.go +++ b/datasource/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestDynamicAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestDynamicAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestDynamicAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestDynamicAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestDynamicAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestDynamicAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +371,32 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() @@ -410,8 +417,6 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +428,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index 8f3dbdc21..c7c09f6c2 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -189,3 +189,20 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go index 0da3cc94e..152ef32c0 100644 --- a/datasource/schema/float32_attribute_test.go +++ b/datasource/schema/float32_attribute_test.go @@ -56,8 +56,6 @@ func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -104,8 +102,6 @@ func TestFloat32AttributeFloat32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +134,6 @@ func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +167,6 @@ func TestFloat32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +199,6 @@ func TestFloat32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +231,6 @@ func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +263,6 @@ func TestFloat32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -309,8 +295,6 @@ func TestFloat32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,8 +327,6 @@ func TestFloat32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -377,8 +359,6 @@ func TestFloat32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -411,8 +391,6 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -424,3 +402,81 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 1313353ec..b688653e3 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,20 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/float64_attribute_test.go b/datasource/schema/float64_attribute_test.go index f2e05ebf7..a99d9154c 100644 --- a/datasource/schema/float64_attribute_test.go +++ b/datasource/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestFloat64AttributeFloat64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestFloat64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestFloat64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestFloat64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestFloat64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestFloat64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestFloat64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index 89f852e8c..44c2b631f 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -189,3 +189,20 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go index 3df85e9c7..2e38dc919 100644 --- a/datasource/schema/int32_attribute_test.go +++ b/datasource/schema/int32_attribute_test.go @@ -56,8 +56,6 @@ func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -104,8 +102,6 @@ func TestInt32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +135,6 @@ func TestInt32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +167,6 @@ func TestInt32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +199,6 @@ func TestInt32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +231,6 @@ func TestInt32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +263,6 @@ func TestInt32AttributeInt32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -309,8 +295,6 @@ func TestInt32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,8 +327,6 @@ func TestInt32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -377,8 +359,6 @@ func TestInt32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -411,8 +391,6 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -424,3 +402,81 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index ab9d5ca1b..c5dc49dc7 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,20 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/int64_attribute_test.go b/datasource/schema/int64_attribute_test.go index c1ac3c7a4..c692c2b3f 100644 --- a/datasource/schema/int64_attribute_test.go +++ b/datasource/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestInt64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestInt64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestInt64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestInt64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestInt64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestInt64AttributeInt64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestInt64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestInt64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestInt64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 9d502067f..d581d6638 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -202,11 +203,28 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators } +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/datasource/schema/list_attribute_test.go b/datasource/schema/list_attribute_test.go index 27294a130..d28a3732d 100644 --- a/datasource/schema/list_attribute_test.go +++ b/datasource/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestListAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestListAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestListAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestListAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestListAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +271,6 @@ func TestListAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +303,6 @@ func TestListAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +335,6 @@ func TestListAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +367,6 @@ func TestListAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +379,32 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() @@ -418,8 +425,6 @@ func TestListAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +512,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +524,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index b9b70d6fb..f2b88a87c 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -230,6 +231,23 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_nested_attribute_test.go b/datasource/schema/list_nested_attribute_test.go index 5d1e7db88..70725f0eb 100644 --- a/datasource/schema/list_nested_attribute_test.go +++ b/datasource/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestListNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestListNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestListNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestListNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +422,6 @@ func TestListNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +460,6 @@ func TestListNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +498,6 @@ func TestListNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +536,6 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +548,32 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() @@ -595,8 +600,6 @@ func TestListNestedAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +677,6 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +689,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_nested_block_test.go b/datasource/schema/list_nested_block_test.go index 6dce467fa..dcf2f0d6b 100644 --- a/datasource/schema/list_nested_block_test.go +++ b/datasource/schema/list_nested_block_test.go @@ -86,8 +86,6 @@ func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestListNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestListNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestListNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestListNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestListNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestListNestedBlockListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestListNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestListNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 516576a4e..164133370 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -205,6 +206,23 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_attribute_test.go b/datasource/schema/map_attribute_test.go index d79425462..f72a8b496 100644 --- a/datasource/schema/map_attribute_test.go +++ b/datasource/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestMapAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestMapAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestMapAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestMapAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestMapAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +271,6 @@ func TestMapAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +303,6 @@ func TestMapAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +335,6 @@ func TestMapAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +367,6 @@ func TestMapAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +379,32 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() @@ -418,8 +425,6 @@ func TestMapAttributeMapValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +512,6 @@ func TestMapAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +524,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index 9729efdc3..30c0c2240 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -231,6 +231,23 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_nested_attribute_test.go b/datasource/schema/map_nested_attribute_test.go index ea6dc480d..e5db0a5c2 100644 --- a/datasource/schema/map_nested_attribute_test.go +++ b/datasource/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestMapNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestMapNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestMapNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestMapNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +422,6 @@ func TestMapNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +460,6 @@ func TestMapNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +498,6 @@ func TestMapNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +536,6 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +548,32 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() @@ -595,8 +600,6 @@ func TestMapNestedAttributeMapNestedValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +677,6 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +689,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/nested_attribute_object_test.go b/datasource/schema/nested_attribute_object_test.go index db5bc1277..74bf24506 100644 --- a/datasource/schema/nested_attribute_object_test.go +++ b/datasource/schema/nested_attribute_object_test.go @@ -80,8 +80,6 @@ func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestNestedAttributeObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +181,6 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -223,8 +217,6 @@ func TestNestedAttributeObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -265,8 +257,6 @@ func TestNestedAttributeObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/nested_block_object_test.go b/datasource/schema/nested_block_object_test.go index 2e0aa4738..550b8563f 100644 --- a/datasource/schema/nested_block_object_test.go +++ b/datasource/schema/nested_block_object_test.go @@ -98,8 +98,6 @@ func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -163,8 +161,6 @@ func TestNestedBlockObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -203,8 +199,6 @@ func TestNestedBlockObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -259,8 +253,6 @@ func TestNestedBlockObjectGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -297,8 +289,6 @@ func TestNestedBlockObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -351,8 +341,6 @@ func TestNestedBlockObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index ffe4e0839..fc12b09dd 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -185,6 +186,23 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/datasource/schema/number_attribute_test.go b/datasource/schema/number_attribute_test.go index f8cec323d..87c10c323 100644 --- a/datasource/schema/number_attribute_test.go +++ b/datasource/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestNumberAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestNumberAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestNumberAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestNumberAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestNumberAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestNumberAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestNumberAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestNumberAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +371,32 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() @@ -410,8 +417,6 @@ func TestNumberAttributeNumberValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +428,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index eafa40c6e..21586932a 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -204,6 +205,23 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/object_attribute_test.go b/datasource/schema/object_attribute_test.go index c517465ed..a4b30d2fe 100644 --- a/datasource/schema/object_attribute_test.go +++ b/datasource/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -64,8 +65,6 @@ func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -112,8 +111,6 @@ func TestObjectAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -152,8 +149,6 @@ func TestObjectAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -186,8 +181,6 @@ func TestObjectAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -220,8 +213,6 @@ func TestObjectAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -254,8 +245,6 @@ func TestObjectAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -288,8 +277,6 @@ func TestObjectAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -322,8 +309,6 @@ func TestObjectAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -356,8 +341,6 @@ func TestObjectAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,8 +373,6 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -404,6 +385,32 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() @@ -424,8 +431,6 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -540,8 +545,6 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,3 +557,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/schema_test.go b/datasource/schema/schema_test.go index 48dea22c4..a6eb8ee5c 100644 --- a/datasource/schema/schema_test.go +++ b/datasource/schema/schema_test.go @@ -100,8 +100,6 @@ func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,7 +267,6 @@ func TestSchemaAttributeAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,7 +387,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -448,8 +444,6 @@ func TestSchemaGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -504,8 +498,6 @@ func TestSchemaGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -542,8 +534,6 @@ func TestSchemaGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -580,8 +570,6 @@ func TestSchemaGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -618,8 +606,6 @@ func TestSchemaGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -650,8 +636,6 @@ func TestSchemaGetVersion(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -698,8 +682,6 @@ func TestSchemaType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -839,8 +821,6 @@ func TestSchemaTypeAtPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -972,8 +952,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1029,8 +1007,6 @@ func TestSchemaValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1342,8 +1318,6 @@ func TestSchemaValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 261b02424..638c21345 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -200,6 +201,23 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_attribute_test.go b/datasource/schema/set_attribute_test.go index 793095705..29f2effe1 100644 --- a/datasource/schema/set_attribute_test.go +++ b/datasource/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestSetAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestSetAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestSetAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestSetAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestSetAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +271,6 @@ func TestSetAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +303,6 @@ func TestSetAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +335,6 @@ func TestSetAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +367,6 @@ func TestSetAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +379,32 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() @@ -418,8 +425,6 @@ func TestSetAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +512,6 @@ func TestSetAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +524,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 1b17b8743..4247933f2 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -226,6 +226,23 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_nested_attribute_test.go b/datasource/schema/set_nested_attribute_test.go index 7f7815f70..083eba851 100644 --- a/datasource/schema/set_nested_attribute_test.go +++ b/datasource/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestSetNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestSetNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestSetNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestSetNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +422,6 @@ func TestSetNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +460,6 @@ func TestSetNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +498,6 @@ func TestSetNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +536,6 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +548,32 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() @@ -595,8 +600,6 @@ func TestSetNestedAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +677,6 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +689,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/set_nested_block_test.go b/datasource/schema/set_nested_block_test.go index 40096f374..a868bad72 100644 --- a/datasource/schema/set_nested_block_test.go +++ b/datasource/schema/set_nested_block_test.go @@ -86,8 +86,6 @@ func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestSetNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestSetNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestSetNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestSetNestedBlockSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestSetNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestSetNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index 811e76de4..838c9e333 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -240,6 +240,23 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/single_nested_attribute_test.go b/datasource/schema/single_nested_attribute_test.go index 25a5e565e..73a007390 100644 --- a/datasource/schema/single_nested_attribute_test.go +++ b/datasource/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -81,8 +82,6 @@ func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +171,6 @@ func TestSingleNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -210,8 +207,6 @@ func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +243,6 @@ func TestSingleNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -286,8 +279,6 @@ func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -322,8 +313,6 @@ func TestSingleNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,8 +353,6 @@ func TestSingleNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -402,8 +389,6 @@ func TestSingleNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -440,8 +425,6 @@ func TestSingleNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -478,8 +461,6 @@ func TestSingleNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -516,8 +497,6 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -530,6 +509,32 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() @@ -554,8 +559,6 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -567,3 +570,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/single_nested_block_test.go b/datasource/schema/single_nested_block_test.go index 041f98bfa..0208eb561 100644 --- a/datasource/schema/single_nested_block_test.go +++ b/datasource/schema/single_nested_block_test.go @@ -99,8 +99,6 @@ func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +149,6 @@ func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +249,6 @@ func TestSingleNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -291,8 +285,6 @@ func TestSingleNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +321,6 @@ func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,8 +369,6 @@ func TestSingleNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,8 +405,6 @@ func TestSingleNestedBlockObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -471,8 +457,6 @@ func TestSingleNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index 0c2dd9aba..9a61de9b2 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,23 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/datasource/schema/string_attribute_test.go b/datasource/schema/string_attribute_test.go index d436b9bd1..6ccf2a9bf 100644 --- a/datasource/schema/string_attribute_test.go +++ b/datasource/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestStringAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestStringAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestStringAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestStringAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestStringAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestStringAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestStringAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestStringAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +371,32 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() @@ -410,8 +417,6 @@ func TestStringAttributeStringValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +428,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/diag/diagnostics_test.go b/diag/diagnostics_test.go index 82def641e..cdc71ad7d 100644 --- a/diag/diagnostics_test.go +++ b/diag/diagnostics_test.go @@ -61,7 +61,6 @@ func TestDiagnosticsAddAttributeError(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,7 +122,6 @@ func TestDiagnosticsAddAttributeWarning(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -181,7 +179,6 @@ func TestDiagnosticsAddError(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -239,7 +236,6 @@ func TestDiagnosticsAddWarning(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -361,7 +357,6 @@ func TestDiagnosticsAppend(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -462,7 +457,6 @@ func TestDiagnosticsContains(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -588,8 +582,6 @@ func TestDiagnosticsEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -637,7 +629,6 @@ func TestDiagnosticsHasError(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -682,7 +673,6 @@ func TestDiagnosticsErrorsCount(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -727,7 +717,6 @@ func TestDiagnosticsWarningsCount(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -774,7 +763,6 @@ func TestDiagnosticsErrors(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -821,7 +809,6 @@ func TestDiagnosticsWarnings(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/diag/error_diagnostic_test.go b/diag/error_diagnostic_test.go index 2aaa44ecd..9746e6776 100644 --- a/diag/error_diagnostic_test.go +++ b/diag/error_diagnostic_test.go @@ -45,7 +45,6 @@ func TestErrorDiagnosticEqual(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/diag/warning_diagnostic_test.go b/diag/warning_diagnostic_test.go index 856346df0..177ed2082 100644 --- a/diag/warning_diagnostic_test.go +++ b/diag/warning_diagnostic_test.go @@ -45,7 +45,6 @@ func TestWarningDiagnosticEqual(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/doc.go b/ephemeral/doc.go index 2537d610c..02b0e6e3a 100644 --- a/ephemeral/doc.go +++ b/ephemeral/doc.go @@ -20,7 +20,4 @@ // that has its own configuration and lifecycle logic. The [ephemeral.EphemeralResource] // implementations are referenced by the [provider.ProviderWithEphemeralResources] type // EphemeralResources method, which enables the ephemeral resource practitioner usage. -// -// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until -// these notices are removed. package ephemeral diff --git a/ephemeral/ephemeral_resource.go b/ephemeral/ephemeral_resource.go index c6cd2b32d..b15707f3b 100644 --- a/ephemeral/ephemeral_resource.go +++ b/ephemeral/ephemeral_resource.go @@ -24,9 +24,6 @@ import ( // HashiCorp Vault leases, which can be renewed without changing their data. // // - Close: Allows providers to clean up the ephemeral resource via EphemeralResourceWithClose. -// -// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until -// these notices are removed. type EphemeralResource interface { // Metadata should return the full name of the ephemeral resource, such as // examplecloud_thing. diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index 47e248aae..f8ebd5ef7 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -183,3 +184,21 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go index 077ee1f1f..7a9b27f63 100644 --- a/ephemeral/schema/bool_attribute_test.go +++ b/ephemeral/schema/bool_attribute_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -55,8 +56,6 @@ func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestBoolAttributeBoolValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestBoolAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestBoolAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestBoolAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestBoolAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestBoolAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestBoolAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestBoolAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestBoolAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/doc.go b/ephemeral/schema/doc.go index 93c0835e2..12f7c3362 100644 --- a/ephemeral/schema/doc.go +++ b/ephemeral/schema/doc.go @@ -5,7 +5,4 @@ // Ephemeral resource schemas define the structure and value types for configuration // and result data. Schemas are implemented via the ephemeral.EphemeralResource type // Schema method. -// -// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until -// these notices are removed. package schema diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index 5088fef63..339775fe6 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -180,7 +181,25 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go index a718167a6..48fc4da08 100644 --- a/ephemeral/schema/dynamic_attribute_test.go +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -55,8 +55,6 @@ func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +101,6 @@ func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +134,6 @@ func TestDynamicAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +166,6 @@ func TestDynamicAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +198,6 @@ func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +230,6 @@ func TestDynamicAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestDynamicAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +294,6 @@ func TestDynamicAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +326,6 @@ func TestDynamicAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +358,6 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +370,32 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() @@ -410,8 +416,6 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +427,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index 93ce2ac7d..733502fef 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -187,3 +187,21 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go index e9e45d785..90f9afe99 100644 --- a/ephemeral/schema/float32_attribute_test.go +++ b/ephemeral/schema/float32_attribute_test.go @@ -56,8 +56,6 @@ func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -104,8 +102,6 @@ func TestFloat32AttributeFloat32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +134,6 @@ func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +167,6 @@ func TestFloat32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +199,6 @@ func TestFloat32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +231,6 @@ func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +263,6 @@ func TestFloat32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -309,8 +295,6 @@ func TestFloat32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,8 +327,6 @@ func TestFloat32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -377,8 +359,6 @@ func TestFloat32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -411,8 +391,6 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -424,3 +402,81 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index ff1912d1e..9c72720f1 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -186,3 +187,21 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go index 4c3f2703a..29b240f7c 100644 --- a/ephemeral/schema/float64_attribute_test.go +++ b/ephemeral/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestFloat64AttributeFloat64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestFloat64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestFloat64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestFloat64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestFloat64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestFloat64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestFloat64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat54AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index f98cbe3b0..c493b08c3 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -187,3 +187,21 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go index bd6d4d36c..3e88ea4d6 100644 --- a/ephemeral/schema/int32_attribute_test.go +++ b/ephemeral/schema/int32_attribute_test.go @@ -56,8 +56,6 @@ func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -104,8 +102,6 @@ func TestInt32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +135,6 @@ func TestInt32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +167,6 @@ func TestInt32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +199,6 @@ func TestInt32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +231,6 @@ func TestInt32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +263,6 @@ func TestInt32AttributeInt32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -309,8 +295,6 @@ func TestInt32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,8 +327,6 @@ func TestInt32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -377,8 +359,6 @@ func TestInt32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -411,8 +391,6 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -424,3 +402,81 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt2AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index 52b1b7ffd..c356ac65b 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -186,3 +187,21 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go index e61165c5f..ef637140f 100644 --- a/ephemeral/schema/int64_attribute_test.go +++ b/ephemeral/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestInt64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestInt64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestInt64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestInt64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestInt64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestInt64AttributeInt64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestInt64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestInt64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestInt64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +391,6 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +402,81 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 3846316b6..246e77a78 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -74,7 +75,6 @@ type ListAttribute struct { // Sensitive indicates whether the value of this attribute should be // considered sensitive data. Setting it to true will obscure the value - // in CLI output. Sensitive bool // Description is used in various tooling, like the language server, to @@ -200,6 +200,24 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go index 1b22bbbdc..9ee0e5c85 100644 --- a/ephemeral/schema/list_attribute_test.go +++ b/ephemeral/schema/list_attribute_test.go @@ -58,8 +58,6 @@ func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +104,6 @@ func TestListAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +142,6 @@ func TestListAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +174,6 @@ func TestListAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +206,6 @@ func TestListAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +238,6 @@ func TestListAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +270,6 @@ func TestListAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +302,6 @@ func TestListAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +334,6 @@ func TestListAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +366,6 @@ func TestListAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +398,6 @@ func TestListAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +485,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +497,81 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index 4a91f2742..a70b6835f 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -228,6 +229,24 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go index 3d1ae651d..daba7e113 100644 --- a/ephemeral/schema/list_nested_attribute_test.go +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -86,8 +86,6 @@ func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +227,6 @@ func TestListNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +265,6 @@ func TestListNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +303,6 @@ func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +339,6 @@ func TestListNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +383,6 @@ func TestListNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +421,6 @@ func TestListNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +459,6 @@ func TestListNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +497,6 @@ func TestListNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +535,6 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +547,32 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() @@ -595,8 +599,6 @@ func TestListNestedAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +676,6 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +688,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_block_test.go b/ephemeral/schema/list_nested_block_test.go index bb20c35de..763afd28c 100644 --- a/ephemeral/schema/list_nested_block_test.go +++ b/ephemeral/schema/list_nested_block_test.go @@ -86,8 +86,6 @@ func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestListNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestListNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestListNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestListNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestListNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestListNestedBlockListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestListNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestListNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 366619ed5..86480e01f 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -203,6 +204,24 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go index 94219da83..ed2348551 100644 --- a/ephemeral/schema/map_attribute_test.go +++ b/ephemeral/schema/map_attribute_test.go @@ -58,8 +58,6 @@ func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +104,6 @@ func TestMapAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +142,6 @@ func TestMapAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +174,6 @@ func TestMapAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +206,6 @@ func TestMapAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +238,6 @@ func TestMapAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +270,6 @@ func TestMapAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +302,6 @@ func TestMapAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +334,6 @@ func TestMapAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +366,6 @@ func TestMapAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +378,32 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() @@ -418,8 +424,6 @@ func TestMapAttributeMapValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +511,6 @@ func TestMapAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +523,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index 11d701cc9..6e8df3dde 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -229,6 +229,24 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go index 0e7986f89..2bbaa15d5 100644 --- a/ephemeral/schema/map_nested_attribute_test.go +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -86,8 +86,6 @@ func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +227,6 @@ func TestMapNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +265,6 @@ func TestMapNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +303,6 @@ func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +339,6 @@ func TestMapNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +383,6 @@ func TestMapNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +421,6 @@ func TestMapNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +459,6 @@ func TestMapNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +497,6 @@ func TestMapNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +535,6 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +547,32 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() @@ -595,8 +599,6 @@ func TestMapNestedAttributeMapNestedValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +676,6 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +688,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/nested_attribute_object_test.go b/ephemeral/schema/nested_attribute_object_test.go index 65c76544b..f4827f212 100644 --- a/ephemeral/schema/nested_attribute_object_test.go +++ b/ephemeral/schema/nested_attribute_object_test.go @@ -80,8 +80,6 @@ func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestNestedAttributeObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +181,6 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -223,8 +217,6 @@ func TestNestedAttributeObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -265,8 +257,6 @@ func TestNestedAttributeObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/nested_block_object_test.go b/ephemeral/schema/nested_block_object_test.go index f484d10ca..7c2ebffc5 100644 --- a/ephemeral/schema/nested_block_object_test.go +++ b/ephemeral/schema/nested_block_object_test.go @@ -98,8 +98,6 @@ func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -163,8 +161,6 @@ func TestNestedBlockObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -203,8 +199,6 @@ func TestNestedBlockObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -259,8 +253,6 @@ func TestNestedBlockObjectGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -297,8 +289,6 @@ func TestNestedBlockObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -351,8 +341,6 @@ func TestNestedBlockObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index 547396409..d56a4ddb1 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -183,6 +184,24 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/ephemeral/schema/number_attribute_test.go b/ephemeral/schema/number_attribute_test.go index 7e326b90e..2b07635a6 100644 --- a/ephemeral/schema/number_attribute_test.go +++ b/ephemeral/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestNumberAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestNumberAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestNumberAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestNumberAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestNumberAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestNumberAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +295,6 @@ func TestNumberAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +327,6 @@ func TestNumberAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +359,6 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +371,32 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() @@ -410,8 +417,6 @@ func TestNumberAttributeNumberValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +428,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index 1ff506f9d..cab38b7e8 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -76,7 +77,6 @@ type ObjectAttribute struct { // Sensitive indicates whether the value of this attribute should be // considered sensitive data. Setting it to true will obscure the value - // in CLI output. Sensitive bool // Description is used in various tooling, like the language server, to @@ -202,6 +202,24 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go index 429aba1e7..cff690c6e 100644 --- a/ephemeral/schema/object_attribute_test.go +++ b/ephemeral/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -64,8 +65,6 @@ func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -112,8 +111,6 @@ func TestObjectAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -152,8 +149,6 @@ func TestObjectAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -186,8 +181,6 @@ func TestObjectAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -220,8 +213,6 @@ func TestObjectAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -254,8 +245,6 @@ func TestObjectAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -288,8 +277,6 @@ func TestObjectAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -322,8 +309,6 @@ func TestObjectAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -356,8 +341,6 @@ func TestObjectAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,8 +373,6 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -424,8 +405,6 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -438,6 +417,32 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -540,8 +545,6 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,3 +557,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/schema_test.go b/ephemeral/schema/schema_test.go index 64cc4806b..e08d76a5d 100644 --- a/ephemeral/schema/schema_test.go +++ b/ephemeral/schema/schema_test.go @@ -100,8 +100,6 @@ func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,7 +267,6 @@ func TestSchemaAttributeAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,7 +387,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -448,8 +444,6 @@ func TestSchemaGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -504,8 +498,6 @@ func TestSchemaGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -542,8 +534,6 @@ func TestSchemaGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -580,8 +570,6 @@ func TestSchemaGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -618,8 +606,6 @@ func TestSchemaGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -650,8 +636,6 @@ func TestSchemaGetVersion(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -698,8 +682,6 @@ func TestSchemaType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -839,8 +821,6 @@ func TestSchemaTypeAtPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -972,8 +952,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1029,8 +1007,6 @@ func TestSchemaValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1342,8 +1318,6 @@ func TestSchemaValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index 7f6a408ed..963253f91 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,6 +199,24 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go index 0b6903829..af56ab903 100644 --- a/ephemeral/schema/set_attribute_test.go +++ b/ephemeral/schema/set_attribute_test.go @@ -58,8 +58,6 @@ func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +104,6 @@ func TestSetAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +142,6 @@ func TestSetAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +174,6 @@ func TestSetAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +206,6 @@ func TestSetAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +238,6 @@ func TestSetAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +270,6 @@ func TestSetAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +302,6 @@ func TestSetAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +334,6 @@ func TestSetAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +366,6 @@ func TestSetAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +378,32 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() @@ -418,8 +424,6 @@ func TestSetAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +511,6 @@ func TestSetAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,3 +523,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index 7ed6d2fdf..0e1dc40af 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -224,6 +224,24 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go index 1bd3daa65..a049ade74 100644 --- a/ephemeral/schema/set_nested_attribute_test.go +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestSetNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestSetNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestSetNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestSetNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -435,8 +422,6 @@ func TestSetNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +460,6 @@ func TestSetNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,8 +498,6 @@ func TestSetNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -555,8 +536,6 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,6 +548,32 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() @@ -595,8 +600,6 @@ func TestSetNestedAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -674,8 +677,6 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -688,3 +689,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_nested_block_test.go b/ephemeral/schema/set_nested_block_test.go index 0862026f4..d403d4486 100644 --- a/ephemeral/schema/set_nested_block_test.go +++ b/ephemeral/schema/set_nested_block_test.go @@ -86,8 +86,6 @@ func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestSetNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestSetNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestSetNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestSetNestedBlockSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestSetNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestSetNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index a57db9c65..23bc8cf11 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -238,6 +238,24 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/single_nested_attribute_test.go b/ephemeral/schema/single_nested_attribute_test.go index 5b7209edb..f97ba872b 100644 --- a/ephemeral/schema/single_nested_attribute_test.go +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -81,8 +81,6 @@ func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +170,6 @@ func TestSingleNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -210,8 +206,6 @@ func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +242,6 @@ func TestSingleNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -286,8 +278,6 @@ func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -322,8 +312,6 @@ func TestSingleNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,8 +352,6 @@ func TestSingleNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -402,8 +388,6 @@ func TestSingleNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -440,8 +424,6 @@ func TestSingleNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -478,8 +460,6 @@ func TestSingleNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -516,8 +496,6 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -530,6 +508,32 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() @@ -554,8 +558,6 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -567,3 +569,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_block_test.go b/ephemeral/schema/single_nested_block_test.go index 994f94894..6c6a74639 100644 --- a/ephemeral/schema/single_nested_block_test.go +++ b/ephemeral/schema/single_nested_block_test.go @@ -98,8 +98,6 @@ func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -150,8 +148,6 @@ func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -252,8 +248,6 @@ func TestSingleNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -290,8 +284,6 @@ func TestSingleNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +320,6 @@ func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestSingleNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -416,8 +404,6 @@ func TestSingleNestedBlockObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -470,8 +456,6 @@ func TestSingleNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index 1b5c7db22..ec8f8692f 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -179,6 +180,24 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/ephemeral/schema/string_attribute_test.go b/ephemeral/schema/string_attribute_test.go index 1c95add7d..95a34c24c 100644 --- a/ephemeral/schema/string_attribute_test.go +++ b/ephemeral/schema/string_attribute_test.go @@ -55,8 +55,6 @@ func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +101,6 @@ func TestStringAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +134,6 @@ func TestStringAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +166,6 @@ func TestStringAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +198,6 @@ func TestStringAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +230,6 @@ func TestStringAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestStringAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +294,6 @@ func TestStringAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -342,8 +326,6 @@ func TestStringAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,8 +358,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,6 +370,32 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() @@ -410,8 +416,6 @@ func TestStringAttributeStringValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,3 +427,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/arguments_data_test.go b/function/arguments_data_test.go index 7b8d2f83e..6c8e55fb9 100644 --- a/function/arguments_data_test.go +++ b/function/arguments_data_test.go @@ -68,8 +68,6 @@ func TestArgumentsDataEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +355,6 @@ func TestArgumentsDataGet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +460,6 @@ func TestArgumentsDataGetArgument(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/bool_parameter_test.go b/function/bool_parameter_test.go index 6082fc279..2495bd19a 100644 --- a/function/bool_parameter_test.go +++ b/function/bool_parameter_test.go @@ -44,8 +44,6 @@ func TestBoolParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestBoolParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestBoolParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestBoolParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestBoolParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestBoolParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestBoolParameterBoolValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestBoolParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/bool_return_test.go b/function/bool_return_test.go index 5d27e7429..3894721a1 100644 --- a/function/bool_return_test.go +++ b/function/bool_return_test.go @@ -33,8 +33,6 @@ func TestBoolReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/definition_test.go b/function/definition_test.go index 87dcc27e6..d4625b685 100644 --- a/function/definition_test.go +++ b/function/definition_test.go @@ -293,8 +293,6 @@ func TestDefinitionValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/dynamic_parameter_test.go b/function/dynamic_parameter_test.go index d79889c83..94b91d0d0 100644 --- a/function/dynamic_parameter_test.go +++ b/function/dynamic_parameter_test.go @@ -44,8 +44,6 @@ func TestDynamicParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestDynamicParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestDynamicParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestDynamicParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestDynamicParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestDynamicParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestDynamicParameterDynamicValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestDynamicParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/dynamic_return_test.go b/function/dynamic_return_test.go index 1c1941050..67eaf918d 100644 --- a/function/dynamic_return_test.go +++ b/function/dynamic_return_test.go @@ -33,8 +33,6 @@ func TestDynamicReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/float32_parameter_test.go b/function/float32_parameter_test.go index 9292bf8c1..6dc40f894 100644 --- a/function/float32_parameter_test.go +++ b/function/float32_parameter_test.go @@ -44,8 +44,6 @@ func TestFloat32ParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestFloat32ParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestFloat32ParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestFloat32ParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestFloat32ParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestFloat32ParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestFloat32ParameterFloat32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestFloat32ParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/float32_return_test.go b/function/float32_return_test.go index 242041ebe..fe26963f2 100644 --- a/function/float32_return_test.go +++ b/function/float32_return_test.go @@ -34,8 +34,6 @@ func TestFloat32ReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/float64_parameter_test.go b/function/float64_parameter_test.go index 1e99b0904..60dc4495c 100644 --- a/function/float64_parameter_test.go +++ b/function/float64_parameter_test.go @@ -44,8 +44,6 @@ func TestFloat64ParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestFloat64ParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestFloat64ParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestFloat64ParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestFloat64ParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestFloat64ParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestFloat64ParameterFloat64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestFloat64ParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/float64_return_test.go b/function/float64_return_test.go index 8b48732c4..31a682920 100644 --- a/function/float64_return_test.go +++ b/function/float64_return_test.go @@ -33,8 +33,6 @@ func TestFloat64ReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/func_error_test.go b/function/func_error_test.go index 6232071e0..b95d89bb9 100644 --- a/function/func_error_test.go +++ b/function/func_error_test.go @@ -74,7 +74,6 @@ func TestFunctionError_Equal(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,7 +109,6 @@ func TestFunctionError_Error(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -219,7 +217,6 @@ func TestConcatFuncErrors(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -310,7 +307,6 @@ func TestFuncErrorFromDiags(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/int32_parameter_test.go b/function/int32_parameter_test.go index 29bb0e29d..880e8c1e0 100644 --- a/function/int32_parameter_test.go +++ b/function/int32_parameter_test.go @@ -44,8 +44,6 @@ func TestInt32ParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestInt32ParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestInt32ParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestInt32ParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestInt32ParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestInt32ParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestInt32ParameterInt32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestInt32ParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/int32_return_test.go b/function/int32_return_test.go index 555fe0c1a..ef13ca85f 100644 --- a/function/int32_return_test.go +++ b/function/int32_return_test.go @@ -34,8 +34,6 @@ func TestInt32ReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/int64_parameter_test.go b/function/int64_parameter_test.go index a0880de07..d4b9c59e1 100644 --- a/function/int64_parameter_test.go +++ b/function/int64_parameter_test.go @@ -44,8 +44,6 @@ func TestInt64ParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestInt64ParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestInt64ParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestInt64ParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestInt64ParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestInt64ParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestInt64ParameterInt64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestInt64ParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/int64_return_test.go b/function/int64_return_test.go index 7f15e3006..eae8b4bb5 100644 --- a/function/int64_return_test.go +++ b/function/int64_return_test.go @@ -33,8 +33,6 @@ func TestInt64ReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/list_parameter_test.go b/function/list_parameter_test.go index abca3b288..630053b20 100644 --- a/function/list_parameter_test.go +++ b/function/list_parameter_test.go @@ -45,8 +45,6 @@ func TestListParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -85,8 +83,6 @@ func TestListParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -125,8 +121,6 @@ func TestListParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +159,6 @@ func TestListParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +191,6 @@ func TestListParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -245,8 +235,6 @@ func TestListParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestListParameterListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,8 +409,6 @@ func TestListParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/list_return_test.go b/function/list_return_test.go index 1465597d3..aaa1b97ab 100644 --- a/function/list_return_test.go +++ b/function/list_return_test.go @@ -49,8 +49,6 @@ func TestListReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestListReturnValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/map_parameter_test.go b/function/map_parameter_test.go index 003f03f0b..f58e30193 100644 --- a/function/map_parameter_test.go +++ b/function/map_parameter_test.go @@ -45,8 +45,6 @@ func TestMapParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -85,8 +83,6 @@ func TestMapParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -125,8 +121,6 @@ func TestMapParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +159,6 @@ func TestMapParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +191,6 @@ func TestMapParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -245,8 +235,6 @@ func TestMapParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestMapParameterMapValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,8 +409,6 @@ func TestMapParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/map_return_test.go b/function/map_return_test.go index b919231c2..e0b8a9180 100644 --- a/function/map_return_test.go +++ b/function/map_return_test.go @@ -49,8 +49,6 @@ func TestMapReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestMapReturnValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/number_parameter_test.go b/function/number_parameter_test.go index d5fbc23f1..df6d09db5 100644 --- a/function/number_parameter_test.go +++ b/function/number_parameter_test.go @@ -44,8 +44,6 @@ func TestNumberParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestNumberParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestNumberParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestNumberParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestNumberParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestNumberParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestNumberParameterNumberValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestNumberParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/number_return_test.go b/function/number_return_test.go index 870548470..f7495b723 100644 --- a/function/number_return_test.go +++ b/function/number_return_test.go @@ -33,8 +33,6 @@ func TestNumberReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/object_parameter_test.go b/function/object_parameter_test.go index 79c86e234..2cb78d05d 100644 --- a/function/object_parameter_test.go +++ b/function/object_parameter_test.go @@ -45,8 +45,6 @@ func TestObjectParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -85,8 +83,6 @@ func TestObjectParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -125,8 +121,6 @@ func TestObjectParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +159,6 @@ func TestObjectParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +191,6 @@ func TestObjectParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +243,6 @@ func TestObjectParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +283,6 @@ func TestObjectParameterObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -479,8 +465,6 @@ func TestObjectParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/object_return_test.go b/function/object_return_test.go index af1c78652..5e984c3b2 100644 --- a/function/object_return_test.go +++ b/function/object_return_test.go @@ -57,8 +57,6 @@ func TestObjectReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +163,6 @@ func TestObjectReturnValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/result_data_test.go b/function/result_data_test.go index 7bd59010a..f32cbf560 100644 --- a/function/result_data_test.go +++ b/function/result_data_test.go @@ -54,8 +54,6 @@ func TestResultDataSet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/set_parameter_test.go b/function/set_parameter_test.go index 705640dbb..eeef290ab 100644 --- a/function/set_parameter_test.go +++ b/function/set_parameter_test.go @@ -45,8 +45,6 @@ func TestSetParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -85,8 +83,6 @@ func TestSetParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -125,8 +121,6 @@ func TestSetParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +159,6 @@ func TestSetParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +191,6 @@ func TestSetParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -245,8 +235,6 @@ func TestSetParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestSetParameterSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -423,8 +409,6 @@ func TestSetParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/set_return_test.go b/function/set_return_test.go index ef973f3f6..9623a4cfd 100644 --- a/function/set_return_test.go +++ b/function/set_return_test.go @@ -49,8 +49,6 @@ func TestSetReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestSetReturnValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/string_parameter_test.go b/function/string_parameter_test.go index 73a8a18cb..3fe3b04b7 100644 --- a/function/string_parameter_test.go +++ b/function/string_parameter_test.go @@ -44,8 +44,6 @@ func TestStringParameterGetAllowNullValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -84,8 +82,6 @@ func TestStringParameterGetAllowUnknownValues(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -124,8 +120,6 @@ func TestStringParameterGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +158,6 @@ func TestStringParameterGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestStringParameterGetName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -232,8 +222,6 @@ func TestStringParameterGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +262,6 @@ func TestStringParameterStringValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestStringParameterValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/function/string_return_test.go b/function/string_return_test.go index 5e0db2260..08c867884 100644 --- a/function/string_return_test.go +++ b/function/string_return_test.go @@ -33,8 +33,6 @@ func TestStringReturnGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 193d48e39..68402e15c 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/hashicorp/terraform-plugin-framework -go 1.22.0 +go 1.23.0 -toolchain go1.22.7 +toolchain go1.23.7 require ( - github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.25.0 + github.com/google/go-cmp v0.7.0 + github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -14,9 +14,9 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/terraform-registry-address v0.2.3 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // 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.12 // indirect @@ -25,10 +25,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index 99e7874be..cfd3d364a 100644 --- a/go.sum +++ b/go.sum @@ -5,22 +5,28 @@ 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= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -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-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= -github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1 h1:/IZFNUEafGnJGXRe2iNQQ+vtzEw/5qiD+gOxkFrNbi4= +github.com/hashicorp/terraform-plugin-go v0.27.0-alpha.1/go.mod h1:Tf2HngbyKvovAlGXgBOVGm3EDvbNaN/StUaTXwrej4o= 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-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= -github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +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-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= @@ -48,24 +54,36 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +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/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index 08d04d4ac..c32b34387 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov5.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -40,6 +40,7 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso fw := &fwserver.ApplyResourceChangeRequest{ ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: resource, } @@ -55,6 +56,12 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso fw.PlannedState = plannedState + plannedIdentity, plannedIdentityDiags := ResourceIdentity(ctx, proto5.PlannedIdentity, identitySchema) + + diags.Append(plannedIdentityDiags...) + + fw.PlannedIdentity = plannedIdentity + priorState, priorStateDiags := State(ctx, proto5.PriorState, resourceSchema) diags.Append(priorStateDiags...) diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index 6aca21466..a6b09e011 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -61,6 +86,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema + identitySchema fwschema.Schema expected *fwserver.ApplyResourceChangeRequest expectedDiagnostics diag.Diagnostics }{ @@ -137,6 +163,42 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "plannedidentity-missing-schema": { + input: &tfprotov5.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedidentity": { + input: &tfprotov5.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "plannedprivate-malformed-json": { input: &tfprotov5.ApplyResourceChangeRequest{ PlannedPrivate: []byte(`{`), @@ -250,12 +312,10 @@ func TestApplyResourceChangeRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/arguments_data_test.go b/internal/fromproto5/arguments_data_test.go index cf5754adf..25d8c1bcb 100644 --- a/internal/fromproto5/arguments_data_test.go +++ b/internal/fromproto5/arguments_data_test.go @@ -689,8 +689,6 @@ func TestArgumentsData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3114,8 +3112,6 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/callfunction_test.go b/internal/fromproto5/callfunction_test.go index 3cde3f00a..214eebb3a 100644 --- a/internal/fromproto5/callfunction_test.go +++ b/internal/fromproto5/callfunction_test.go @@ -81,8 +81,6 @@ func TestCallFunctionRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 0eaf40c50..737354888 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -89,3 +89,16 @@ func OpenEphemeralResourceClientCapabilities(in *tfprotov5.OpenEphemeralResource DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceTypeConfigClientCapabilities(in *tfprotov5.ValidateResourceTypeConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto5/closeephemeralresource_test.go b/internal/fromproto5/closeephemeralresource_test.go index e9ba362ae..777d2c14b 100644 --- a/internal/fromproto5/closeephemeralresource_test.go +++ b/internal/fromproto5/closeephemeralresource_test.go @@ -82,8 +82,6 @@ func TestCloseEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/config_test.go b/internal/fromproto5/config_test.go index 60e5fc4b4..f252af7a3 100644 --- a/internal/fromproto5/config_test.go +++ b/internal/fromproto5/config_test.go @@ -103,8 +103,6 @@ func TestConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/configureprovider_test.go b/internal/fromproto5/configureprovider_test.go index f61f925a3..b653cd146 100644 --- a/internal/fromproto5/configureprovider_test.go +++ b/internal/fromproto5/configureprovider_test.go @@ -118,8 +118,6 @@ func TestConfigureProviderRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/dynamic_value_test.go b/internal/fromproto5/dynamic_value_test.go index a92d94445..a8aebd7b5 100644 --- a/internal/fromproto5/dynamic_value_test.go +++ b/internal/fromproto5/dynamic_value_test.go @@ -1498,8 +1498,6 @@ func TestDynamicValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/ephemeral_result_data_test.go b/internal/fromproto5/ephemeral_result_data_test.go index 40f2ebff5..0af6479bd 100644 --- a/internal/fromproto5/ephemeral_result_data_test.go +++ b/internal/fromproto5/ephemeral_result_data_test.go @@ -103,8 +103,6 @@ func TestEphemeralResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/getfunctions_test.go b/internal/fromproto5/getfunctions_test.go index 62d42aab8..9e26cbcef 100644 --- a/internal/fromproto5/getfunctions_test.go +++ b/internal/fromproto5/getfunctions_test.go @@ -31,8 +31,6 @@ func TestGetFunctionsRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/getmetadata_test.go b/internal/fromproto5/getmetadata_test.go index e92a5acef..adc450e1d 100644 --- a/internal/fromproto5/getmetadata_test.go +++ b/internal/fromproto5/getmetadata_test.go @@ -31,8 +31,6 @@ func TestGetMetadataRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/getproviderschema_test.go b/internal/fromproto5/getproviderschema_test.go index a9722d84d..754e68605 100644 --- a/internal/fromproto5/getproviderschema_test.go +++ b/internal/fromproto5/getproviderschema_test.go @@ -31,8 +31,6 @@ func TestGetProviderSchemaRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/getresourceidentityschemas.go b/internal/fromproto5/getresourceidentityschemas.go new file mode 100644 index 000000000..a5ca3b16c --- /dev/null +++ b/internal/fromproto5/getresourceidentityschemas.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemasRequest returns the *fwserver.GetResourceIdentitySchemasRequest +// equivalent of a *tfprotov5.GetResourceIdentitySchemasRequest. +func GetResourceIdentitySchemasRequest(ctx context.Context, proto5 *tfprotov5.GetResourceIdentitySchemasRequest) *fwserver.GetResourceIdentitySchemasRequest { + if proto5 == nil { + return nil + } + + fw := &fwserver.GetResourceIdentitySchemasRequest{} + + return fw +} diff --git a/internal/fromproto5/getresourceidentityschemas_test.go b/internal/fromproto5/getresourceidentityschemas_test.go new file mode 100644 index 000000000..e3143c24d --- /dev/null +++ b/internal/fromproto5/getresourceidentityschemas_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestGetResourceIdentitySchemasRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.GetResourceIdentitySchemasRequest + expected *fwserver.GetResourceIdentitySchemasRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.GetResourceIdentitySchemasRequest{}, + expected: &fwserver.GetResourceIdentitySchemasRequest{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto5.GetResourceIdentitySchemasRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/importresourcestate.go b/internal/fromproto5/importresourcestate.go index ec40c2119..04034b584 100644 --- a/internal/fromproto5/importresourcestate.go +++ b/internal/fromproto5/importresourcestate.go @@ -18,7 +18,7 @@ import ( // ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest // equivalent of a *tfprotov5.ImportResourceStateRequest. -func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { +func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes Schema: resourceSchema, }, ID: proto5.ID, + IdentitySchema: identitySchema, Resource: reqResource, TypeName: proto5.TypeName, ClientCapabilities: ImportStateClientCapabilities(proto5.ClientCapabilities), } + identity, identityDiags := ResourceIdentity(ctx, proto5.Identity, identitySchema) + + diags.Append(identityDiags...) + + fw.Identity = identity + return fw, diags } diff --git a/internal/fromproto5/importresourcestate_test.go b/internal/fromproto5/importresourcestate_test.go index ba58522af..737d9a80d 100644 --- a/internal/fromproto5/importresourcestate_test.go +++ b/internal/fromproto5/importresourcestate_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testFwEmptyState := tfsdk.State{ Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil), Schema: testFwSchema, @@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.ImportResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.ImportResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) { ), }, }, + "identity-missing-schema": { + input: &tfprotov5.ImportResourceStateRequest{ + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "identity": { + input: &tfprotov5.ImportResourceStateRequest{ + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + identitySchema: testIdentitySchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + IdentitySchema: testIdentitySchema, + Identity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + }, + }, "id": { input: &tfprotov5.ImportResourceStateRequest{ ID: "test-id", @@ -119,12 +181,10 @@ func TestImportResourceStateRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/moveresourcestate.go b/internal/fromproto5/moveresourcestate.go index f5e836b7e..988bb0ab1 100644 --- a/internal/fromproto5/moveresourcestate.go +++ b/internal/fromproto5/moveresourcestate.go @@ -5,7 +5,6 @@ package fromproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -17,7 +16,7 @@ import ( // MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest // equivalent of a *tfprotov5.MoveResourceStateRequest. -func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { +func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -38,13 +37,16 @@ func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourc } fw := &fwserver.MoveResourceStateRequest{ - SourceProviderAddress: proto5.SourceProviderAddress, - SourceRawState: (*tfprotov6.RawState)(proto5.SourceState), - SourceSchemaVersion: proto5.SourceSchemaVersion, - SourceTypeName: proto5.SourceTypeName, - TargetResource: resource, - TargetResourceSchema: resourceSchema, - TargetTypeName: proto5.TargetTypeName, + SourceProviderAddress: proto5.SourceProviderAddress, + SourceRawState: (*tfprotov6.RawState)(proto5.SourceState), + SourceSchemaVersion: proto5.SourceSchemaVersion, + SourceTypeName: proto5.SourceTypeName, + TargetResource: resource, + TargetResourceSchema: resourceSchema, + TargetTypeName: proto5.TargetTypeName, + SourceIdentity: (*tfprotov6.RawState)(proto5.SourceIdentity), + SourceIdentitySchemaVersion: proto5.SourceIdentitySchemaVersion, + IdentitySchema: identitySchema, } sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto5.SourcePrivate) diff --git a/internal/fromproto5/moveresourcestate_test.go b/internal/fromproto5/moveresourcestate_test.go index 3c8dd5181..78182cd0e 100644 --- a/internal/fromproto5/moveresourcestate_test.go +++ b/internal/fromproto5/moveresourcestate_test.go @@ -32,6 +32,7 @@ func TestMoveResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.MoveResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.MoveResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -162,15 +163,37 @@ func TestMoveResourceStateRequest(t *testing.T) { TargetTypeName: "examplecloud_thing", }, }, + "SourceIdentity": { + input: &tfprotov5.MoveResourceStateRequest{ + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentity: testNewTfprotov6RawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + TargetResourceSchema: testFwSchema, + }, + }, + "SourceIdentitySchemaVersion": { + input: &tfprotov5.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + TargetResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto5.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/openephemeralresource_test.go b/internal/fromproto5/openephemeralresource_test.go index bd53e32d7..e84a5829c 100644 --- a/internal/fromproto5/openephemeralresource_test.go +++ b/internal/fromproto5/openephemeralresource_test.go @@ -127,8 +127,6 @@ func TestOpenEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/plan_test.go b/internal/fromproto5/plan_test.go index c447a86c9..686280fac 100644 --- a/internal/fromproto5/plan_test.go +++ b/internal/fromproto5/plan_test.go @@ -103,8 +103,6 @@ func TestPlan(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 5bd24c1dd..2255dc35b 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov5.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -41,6 +41,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto5.ClientCapabilities), } @@ -57,6 +58,12 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw.PriorState = priorState + priorIdentity, priorIdentityDiags := ResourceIdentity(ctx, proto5.PriorIdentity, identitySchema) + + diags.Append(priorIdentityDiags...) + + fw.PriorIdentity = priorIdentity + proposedNewState, proposedNewStateDiags := Plan(ctx, proto5.ProposedNewState, resourceSchema) diags.Append(proposedNewStateDiags...) diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index 64dcad0f8..fd37a170f 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -58,6 +83,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { input *tfprotov5.PlanResourceChangeRequest resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.PlanResourceChangeRequest @@ -182,6 +208,42 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "prioridentity-missing-schema": { + input: &tfprotov5.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "prioridentity": { + input: &tfprotov5.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "providermeta-missing-data": { input: &tfprotov5.PlanResourceChangeRequest{}, resourceSchema: testFwSchema, @@ -262,12 +324,10 @@ func TestPlanResourceChangeRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) + got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/prepareproviderconfig_test.go b/internal/fromproto5/prepareproviderconfig_test.go index 3b10c7720..fc0f29a01 100644 --- a/internal/fromproto5/prepareproviderconfig_test.go +++ b/internal/fromproto5/prepareproviderconfig_test.go @@ -89,8 +89,6 @@ func TestValidateProviderConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/providermeta_test.go b/internal/fromproto5/providermeta_test.go index e6f1880cb..d7fcba82e 100644 --- a/internal/fromproto5/providermeta_test.go +++ b/internal/fromproto5/providermeta_test.go @@ -99,8 +99,6 @@ func TestProviderMeta(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/readdatasource_test.go b/internal/fromproto5/readdatasource_test.go index cbc054e2d..bcfc169ea 100644 --- a/internal/fromproto5/readdatasource_test.go +++ b/internal/fromproto5/readdatasource_test.go @@ -163,8 +163,6 @@ func TestReadDataSourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/readresource.go b/internal/fromproto5/readresource.go index 04d4b4d2e..003424579 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov5.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto5.ClientCapabilities), } @@ -35,6 +36,12 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw.CurrentState = currentState + currentIdentity, currentIdentityDiags := ResourceIdentity(ctx, proto5.CurrentIdentity, identitySchema) + + diags.Append(currentIdentityDiags...) + + fw.CurrentIdentity = currentIdentity + providerMeta, providerMetaDiags := ProviderMeta(ctx, proto5.ProviderMeta, providerMetaSchema) diags.Append(providerMetaDiags...) diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index fb19e9041..1c8adb7f6 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestReadResourceRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -59,6 +84,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.ReadResourceRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.ReadResourceRequest @@ -99,6 +125,37 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "currentidentity-missing-schema": { + input: &tfprotov5.ReadResourceRequest{ + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "currentidentity": { + input: &tfprotov5.ReadResourceRequest{ + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.ReadResourceRequest{ + IdentitySchema: testIdentitySchema, + CurrentIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + }, + }, "private-malformed-json": { input: &tfprotov5.ReadResourceRequest{ Private: []byte(`{`), @@ -197,12 +254,10 @@ func TestReadResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/renewephemeralresource_test.go b/internal/fromproto5/renewephemeralresource_test.go index b61aed2ad..56b63d92a 100644 --- a/internal/fromproto5/renewephemeralresource_test.go +++ b/internal/fromproto5/renewephemeralresource_test.go @@ -82,8 +82,6 @@ func TestRenewEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/resource_identity.go b/internal/fromproto5/resource_identity.go new file mode 100644 index 000000000..c78f77866 --- /dev/null +++ b/internal/fromproto5/resource_identity.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ResourceIdentity returns the *tfsdk.ResourceIdentity for a *tfprotov5.ResourceIdentityData and fwschema.Schema. +func ResourceIdentity(ctx context.Context, in *tfprotov5.ResourceIdentityData, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if in == nil { + return nil, nil + } + + return IdentityData(ctx, in.IdentityData, schema) +} + +// IdentityData returns the *tfsdk.ResourceIdentity for a *tfprotov5.DynamicValue and fwschema.Schema. +func IdentityData(ctx context.Context, proto5DynamicValue *tfprotov5.DynamicValue, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if proto5DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto5DynamicValue, schema, fwschemadata.DataDescriptionResourceIdentity) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.ResourceIdentity{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto5/resource_identity_test.go b/internal/fromproto5/resource_identity_test.go new file mode 100644 index 000000000..8e7687140 --- /dev/null +++ b/internal/fromproto5/resource_identity_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ResourceIdentityData + schema fwschema.Schema + expected *tfsdk.ResourceIdentity + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ResourceIdentityData{}, + expected: nil, + }, + "missing-schema": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "invalid-schema": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + schema: testFwSchema, + expected: &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ResourceIdentity(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/state_test.go b/internal/fromproto5/state_test.go index 4a374ff24..17a39f250 100644 --- a/internal/fromproto5/state_test.go +++ b/internal/fromproto5/state_test.go @@ -103,8 +103,6 @@ func TestState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/upgraderesourcestate_test.go b/internal/fromproto5/upgraderesourcestate_test.go index 6ecfe55d4..5cbd9aca8 100644 --- a/internal/fromproto5/upgraderesourcestate_test.go +++ b/internal/fromproto5/upgraderesourcestate_test.go @@ -86,8 +86,6 @@ func TestUpgradeResourceStateRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/validatedatasourceconfig_test.go b/internal/fromproto5/validatedatasourceconfig_test.go index 7becf1f38..417d69945 100644 --- a/internal/fromproto5/validatedatasourceconfig_test.go +++ b/internal/fromproto5/validatedatasourceconfig_test.go @@ -91,8 +91,6 @@ func TestValidateDataSourceConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/validateephemeralresourceconfig_test.go b/internal/fromproto5/validateephemeralresourceconfig_test.go index bf4aec660..b1ec6662a 100644 --- a/internal/fromproto5/validateephemeralresourceconfig_test.go +++ b/internal/fromproto5/validateephemeralresourceconfig_test.go @@ -91,8 +91,6 @@ func TestValidateEphemeralResourceConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/validateresourcetypeconfig.go b/internal/fromproto5/validateresourcetypeconfig.go index ae6e41298..1919f9e9c 100644 --- a/internal/fromproto5/validateresourcetypeconfig.go +++ b/internal/fromproto5/validateresourcetypeconfig.go @@ -6,11 +6,12 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ValidateResourceTypeConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceTypeConfigRequest(ctx context.Context, proto5 *tfprotov5.Va fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceTypeConfigClientCapabilities(proto5.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto5/validateresourcetypeconfig_test.go b/internal/fromproto5/validateresourcetypeconfig_test.go index 8aec17098..dbc7dad1b 100644 --- a/internal/fromproto5/validateresourcetypeconfig_test.go +++ b/internal/fromproto5/validateresourcetypeconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceTypeConfigRequest(t *testing.T) { @@ -88,11 +89,42 @@ func TestValidateResourceTypeConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + ClientCapabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index f48eb856b..5620ffe40 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov6.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -40,6 +40,7 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso fw := &fwserver.ApplyResourceChangeRequest{ ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: resource, } @@ -55,6 +56,12 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso fw.PlannedState = plannedState + plannedIdentity, plannedIdentityDiags := ResourceIdentity(ctx, proto6.PlannedIdentity, identitySchema) + + diags.Append(plannedIdentityDiags...) + + fw.PlannedIdentity = plannedIdentity + priorState, priorStateDiags := State(ctx, proto6.PriorState, resourceSchema) diags.Append(priorStateDiags...) diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index fccbf619a..d29913fb3 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -61,6 +86,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema + identitySchema fwschema.Schema expected *fwserver.ApplyResourceChangeRequest expectedDiagnostics diag.Diagnostics }{ @@ -137,6 +163,42 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "plannedidentity-missing-schema": { + input: &tfprotov6.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedidentity": { + input: &tfprotov6.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "plannedprivate-malformed-json": { input: &tfprotov6.ApplyResourceChangeRequest{ PlannedPrivate: []byte(`{`), @@ -250,12 +312,10 @@ func TestApplyResourceChangeRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/arguments_data_test.go b/internal/fromproto6/arguments_data_test.go index 4a6f7f8b4..ad7a2115a 100644 --- a/internal/fromproto6/arguments_data_test.go +++ b/internal/fromproto6/arguments_data_test.go @@ -690,8 +690,6 @@ func TestArgumentsData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3115,8 +3113,6 @@ func TestArgumentsData_ParameterValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/callfunction_test.go b/internal/fromproto6/callfunction_test.go index e3bbd30fb..dd4c84540 100644 --- a/internal/fromproto6/callfunction_test.go +++ b/internal/fromproto6/callfunction_test.go @@ -81,8 +81,6 @@ func TestCallFunctionRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index cd9c92b9c..d22d81623 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -89,3 +89,16 @@ func OpenEphemeralResourceClientCapabilities(in *tfprotov6.OpenEphemeralResource DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceConfigClientCapabilities(in *tfprotov6.ValidateResourceConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto6/closeephemeralresource_test.go b/internal/fromproto6/closeephemeralresource_test.go index dca9046c7..aab1a56a3 100644 --- a/internal/fromproto6/closeephemeralresource_test.go +++ b/internal/fromproto6/closeephemeralresource_test.go @@ -82,8 +82,6 @@ func TestCloseEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/config_test.go b/internal/fromproto6/config_test.go index d48be4755..d0504b6b8 100644 --- a/internal/fromproto6/config_test.go +++ b/internal/fromproto6/config_test.go @@ -103,8 +103,6 @@ func TestConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/configureprovider_test.go b/internal/fromproto6/configureprovider_test.go index 47a3d778a..f7215daae 100644 --- a/internal/fromproto6/configureprovider_test.go +++ b/internal/fromproto6/configureprovider_test.go @@ -118,8 +118,6 @@ func TestConfigureProviderRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/dynamic_value_test.go b/internal/fromproto6/dynamic_value_test.go index eba43543c..c41ac554c 100644 --- a/internal/fromproto6/dynamic_value_test.go +++ b/internal/fromproto6/dynamic_value_test.go @@ -1498,8 +1498,6 @@ func TestDynamicValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/ephemeral_result_data_test.go b/internal/fromproto6/ephemeral_result_data_test.go index 44396ee59..ac48f3f2b 100644 --- a/internal/fromproto6/ephemeral_result_data_test.go +++ b/internal/fromproto6/ephemeral_result_data_test.go @@ -103,8 +103,6 @@ func TestEphemeralResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/getfunctions_test.go b/internal/fromproto6/getfunctions_test.go index 9269a889b..3e62f1150 100644 --- a/internal/fromproto6/getfunctions_test.go +++ b/internal/fromproto6/getfunctions_test.go @@ -31,8 +31,6 @@ func TestGetFunctionsRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/getmetadata_test.go b/internal/fromproto6/getmetadata_test.go index 20944d614..5eed5f7fc 100644 --- a/internal/fromproto6/getmetadata_test.go +++ b/internal/fromproto6/getmetadata_test.go @@ -31,8 +31,6 @@ func TestGetMetadataRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/getproviderschema_test.go b/internal/fromproto6/getproviderschema_test.go index cd4c1bca3..020c96e10 100644 --- a/internal/fromproto6/getproviderschema_test.go +++ b/internal/fromproto6/getproviderschema_test.go @@ -31,8 +31,6 @@ func TestGetProviderSchemaRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/getresourceidentityschemas.go b/internal/fromproto6/getresourceidentityschemas.go new file mode 100644 index 000000000..448c5b56a --- /dev/null +++ b/internal/fromproto6/getresourceidentityschemas.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemasRequest returns the *fwserver.GetResourceIdentitySchemasRequest +// equivalent of a *tfprotov6.GetResourceIdentitySchemasRequest. +func GetResourceIdentitySchemasRequest(ctx context.Context, proto6 *tfprotov6.GetResourceIdentitySchemasRequest) *fwserver.GetResourceIdentitySchemasRequest { + if proto6 == nil { + return nil + } + + fw := &fwserver.GetResourceIdentitySchemasRequest{} + + return fw +} diff --git a/internal/fromproto6/getresourceidentityschemas_test.go b/internal/fromproto6/getresourceidentityschemas_test.go new file mode 100644 index 000000000..22ab51200 --- /dev/null +++ b/internal/fromproto6/getresourceidentityschemas_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestGetResourceIdentitySchemasRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.GetResourceIdentitySchemasRequest + expected *fwserver.GetResourceIdentitySchemasRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.GetResourceIdentitySchemasRequest{}, + expected: &fwserver.GetResourceIdentitySchemasRequest{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto6.GetResourceIdentitySchemasRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/importresourcestate.go b/internal/fromproto6/importresourcestate.go index 7070901cd..5e62da5c7 100644 --- a/internal/fromproto6/importresourcestate.go +++ b/internal/fromproto6/importresourcestate.go @@ -18,7 +18,7 @@ import ( // ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest // equivalent of a *tfprotov6.ImportResourceStateRequest. -func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { +func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes Schema: resourceSchema, }, ID: proto6.ID, + IdentitySchema: identitySchema, Resource: reqResource, TypeName: proto6.TypeName, ClientCapabilities: ImportStateClientCapabilities(proto6.ClientCapabilities), } + identity, identityDiags := ResourceIdentity(ctx, proto6.Identity, identitySchema) + + diags.Append(identityDiags...) + + fw.Identity = identity + return fw, diags } diff --git a/internal/fromproto6/importresourcestate_test.go b/internal/fromproto6/importresourcestate_test.go index 6c72913f7..c022b3258 100644 --- a/internal/fromproto6/importresourcestate_test.go +++ b/internal/fromproto6/importresourcestate_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testFwEmptyState := tfsdk.State{ Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil), Schema: testFwSchema, @@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.ImportResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.ImportResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) { ), }, }, + "identity-missing-schema": { + input: &tfprotov6.ImportResourceStateRequest{ + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "identity": { + input: &tfprotov6.ImportResourceStateRequest{ + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + identitySchema: testIdentitySchema, + expected: &fwserver.ImportResourceStateRequest{ + EmptyState: testFwEmptyState, + IdentitySchema: testIdentitySchema, + Identity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + }, + }, "id": { input: &tfprotov6.ImportResourceStateRequest{ ID: "test-id", @@ -119,12 +181,10 @@ func TestImportResourceStateRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/moveresourcestate.go b/internal/fromproto6/moveresourcestate.go index 6b31a2cc0..7e85ce9f6 100644 --- a/internal/fromproto6/moveresourcestate.go +++ b/internal/fromproto6/moveresourcestate.go @@ -5,7 +5,6 @@ package fromproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -16,7 +15,7 @@ import ( // MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest // equivalent of a *tfprotov6.MoveResourceStateRequest. -func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { +func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -37,13 +36,16 @@ func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourc } fw := &fwserver.MoveResourceStateRequest{ - SourceProviderAddress: proto6.SourceProviderAddress, - SourceRawState: proto6.SourceState, - SourceSchemaVersion: proto6.SourceSchemaVersion, - SourceTypeName: proto6.SourceTypeName, - TargetResource: resource, - TargetResourceSchema: resourceSchema, - TargetTypeName: proto6.TargetTypeName, + SourceProviderAddress: proto6.SourceProviderAddress, + SourceRawState: proto6.SourceState, + SourceSchemaVersion: proto6.SourceSchemaVersion, + SourceTypeName: proto6.SourceTypeName, + TargetResource: resource, + TargetResourceSchema: resourceSchema, + TargetTypeName: proto6.TargetTypeName, + SourceIdentity: proto6.SourceIdentity, + SourceIdentitySchemaVersion: proto6.SourceIdentitySchemaVersion, + IdentitySchema: identitySchema, } sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto6.SourcePrivate) diff --git a/internal/fromproto6/moveresourcestate_test.go b/internal/fromproto6/moveresourcestate_test.go index 3379a3b6e..d53449a72 100644 --- a/internal/fromproto6/moveresourcestate_test.go +++ b/internal/fromproto6/moveresourcestate_test.go @@ -32,6 +32,7 @@ func TestMoveResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.MoveResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.MoveResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -162,15 +163,37 @@ func TestMoveResourceStateRequest(t *testing.T) { TargetTypeName: "examplecloud_thing", }, }, + "SourceIdentity": { + input: &tfprotov6.MoveResourceStateRequest{ + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + TargetResourceSchema: testFwSchema, + }, + }, + "SourceIdentitySchemaVersion": { + input: &tfprotov6.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + TargetResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto6.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/openephemeralresource_test.go b/internal/fromproto6/openephemeralresource_test.go index beea040e4..a6535741e 100644 --- a/internal/fromproto6/openephemeralresource_test.go +++ b/internal/fromproto6/openephemeralresource_test.go @@ -127,8 +127,6 @@ func TestOpenEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/plan_test.go b/internal/fromproto6/plan_test.go index eb58c0caa..4b5ec495b 100644 --- a/internal/fromproto6/plan_test.go +++ b/internal/fromproto6/plan_test.go @@ -103,8 +103,6 @@ func TestPlan(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index 6b95f0789..4bffe04d1 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov6.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -41,6 +41,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto6.ClientCapabilities), } @@ -57,6 +58,12 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw.PriorState = priorState + priorIdentity, priorIdentityDiags := ResourceIdentity(ctx, proto6.PriorIdentity, identitySchema) + + diags.Append(priorIdentityDiags...) + + fw.PriorIdentity = priorIdentity + proposedNewState, proposedNewStateDiags := Plan(ctx, proto6.ProposedNewState, resourceSchema) diags.Append(proposedNewStateDiags...) diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index 81901c514..3660535ae 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -58,6 +83,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { input *tfprotov6.PlanResourceChangeRequest resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.PlanResourceChangeRequest @@ -182,6 +208,42 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "prioridentity-missing-schema": { + input: &tfprotov6.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "prioridentity": { + input: &tfprotov6.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "providermeta-missing-data": { input: &tfprotov6.PlanResourceChangeRequest{}, resourceSchema: testFwSchema, @@ -262,12 +324,10 @@ func TestPlanResourceChangeRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) + got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/providermeta_test.go b/internal/fromproto6/providermeta_test.go index 8b6501975..9e22f02c7 100644 --- a/internal/fromproto6/providermeta_test.go +++ b/internal/fromproto6/providermeta_test.go @@ -99,8 +99,6 @@ func TestProviderMeta(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/readdatasource_test.go b/internal/fromproto6/readdatasource_test.go index 08ef5124a..656d34e6a 100644 --- a/internal/fromproto6/readdatasource_test.go +++ b/internal/fromproto6/readdatasource_test.go @@ -163,8 +163,6 @@ func TestReadDataSourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/readresource.go b/internal/fromproto6/readresource.go index a42f669d6..11ea6d844 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov6.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto6.ClientCapabilities), } @@ -35,6 +36,12 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw.CurrentState = currentState + currentIdentity, currentIdentityDiags := ResourceIdentity(ctx, proto6.CurrentIdentity, identitySchema) + + diags.Append(currentIdentityDiags...) + + fw.CurrentIdentity = currentIdentity + providerMeta, providerMetaDiags := ProviderMeta(ctx, proto6.ProviderMeta, providerMetaSchema) diags.Append(providerMetaDiags...) diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index 03d6eea86..e24e9f5bc 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestReadResourceRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -59,6 +84,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.ReadResourceRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.ReadResourceRequest @@ -99,6 +125,37 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "currentidentity-missing-schema": { + input: &tfprotov6.ReadResourceRequest{ + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "currentidentity": { + input: &tfprotov6.ReadResourceRequest{ + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.ReadResourceRequest{ + IdentitySchema: testIdentitySchema, + CurrentIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + }, + }, "private-malformed-json": { input: &tfprotov6.ReadResourceRequest{ Private: []byte(`{`), @@ -197,12 +254,10 @@ func TestReadResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/renewephemeralresource_test.go b/internal/fromproto6/renewephemeralresource_test.go index 02779157f..0460abf1b 100644 --- a/internal/fromproto6/renewephemeralresource_test.go +++ b/internal/fromproto6/renewephemeralresource_test.go @@ -82,8 +82,6 @@ func TestRenewEphemeralResourceRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/resource_identity.go b/internal/fromproto6/resource_identity.go new file mode 100644 index 000000000..5dc262332 --- /dev/null +++ b/internal/fromproto6/resource_identity.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ResourceIdentity returns the *tfsdk.ResourceIdentity for a *tfprotov6.ResourceIdentityData and fwschema.Schema. +func ResourceIdentity(ctx context.Context, in *tfprotov6.ResourceIdentityData, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if in == nil { + return nil, nil + } + + return IdentityData(ctx, in.IdentityData, schema) +} + +// IdentityData returns the *tfsdk.ResourceIdentity for a *tfprotov6.DynamicValue and fwschema.Schema. +func IdentityData(ctx context.Context, proto6DynamicValue *tfprotov6.DynamicValue, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if proto6DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto6DynamicValue, schema, fwschemadata.DataDescriptionResourceIdentity) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.ResourceIdentity{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto6/resource_identity_test.go b/internal/fromproto6/resource_identity_test.go new file mode 100644 index 000000000..51724170d --- /dev/null +++ b/internal/fromproto6/resource_identity_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ResourceIdentityData + schema fwschema.Schema + expected *tfsdk.ResourceIdentity + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ResourceIdentityData{}, + expected: nil, + }, + "missing-schema": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "invalid-schema": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + schema: testFwSchema, + expected: &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ResourceIdentity(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/state_test.go b/internal/fromproto6/state_test.go index fe6382af4..ec81a9a48 100644 --- a/internal/fromproto6/state_test.go +++ b/internal/fromproto6/state_test.go @@ -103,8 +103,6 @@ func TestState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/upgraderesourcestate_test.go b/internal/fromproto6/upgraderesourcestate_test.go index f1b30efcd..a857a0eea 100644 --- a/internal/fromproto6/upgraderesourcestate_test.go +++ b/internal/fromproto6/upgraderesourcestate_test.go @@ -86,8 +86,6 @@ func TestUpgradeResourceStateRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/validatedatasourceconfig_test.go b/internal/fromproto6/validatedatasourceconfig_test.go index c6dd84466..9bf3404d7 100644 --- a/internal/fromproto6/validatedatasourceconfig_test.go +++ b/internal/fromproto6/validatedatasourceconfig_test.go @@ -91,8 +91,6 @@ func TestValidateDataSourceConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/validateephemeralresourceconfig_test.go b/internal/fromproto6/validateephemeralresourceconfig_test.go index 39d793335..093757252 100644 --- a/internal/fromproto6/validateephemeralresourceconfig_test.go +++ b/internal/fromproto6/validateephemeralresourceconfig_test.go @@ -91,8 +91,6 @@ func TestValidateEphemeralResourceConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/validateproviderconfig_test.go b/internal/fromproto6/validateproviderconfig_test.go index 96abd6dd9..2b8b872bc 100644 --- a/internal/fromproto6/validateproviderconfig_test.go +++ b/internal/fromproto6/validateproviderconfig_test.go @@ -89,8 +89,6 @@ func TestValidateProviderConfigRequest(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromproto6/validateresourceconfig.go b/internal/fromproto6/validateresourceconfig.go index 1eb65aa87..f3ea643c9 100644 --- a/internal/fromproto6/validateresourceconfig.go +++ b/internal/fromproto6/validateresourceconfig.go @@ -6,11 +6,12 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ValidateResourceConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.Valida fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceConfigClientCapabilities(proto6.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto6/validateresourceconfig_test.go b/internal/fromproto6/validateresourceconfig_test.go index 4e21d432c..b28ea5267 100644 --- a/internal/fromproto6/validateresourceconfig_test.go +++ b/internal/fromproto6/validateresourceconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceConfigRequest(t *testing.T) { @@ -88,11 +89,42 @@ func TestValidateResourceConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + ClientCapabilities: &tfprotov6.ValidateResourceConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromtftypes/attribute_path_step_test.go b/internal/fromtftypes/attribute_path_step_test.go index 4e4a291fb..93086d738 100644 --- a/internal/fromtftypes/attribute_path_step_test.go +++ b/internal/fromtftypes/attribute_path_step_test.go @@ -57,8 +57,6 @@ func TestAttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromtftypes/attribute_path_test.go b/internal/fromtftypes/attribute_path_test.go index 8166c7be7..e650cfdef 100644 --- a/internal/fromtftypes/attribute_path_test.go +++ b/internal/fromtftypes/attribute_path_test.go @@ -182,8 +182,6 @@ func TestAttributePath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fromtftypes/value_test.go b/internal/fromtftypes/value_test.go index d633c8157..4775e1771 100644 --- a/internal/fromtftypes/value_test.go +++ b/internal/fromtftypes/value_test.go @@ -351,8 +351,6 @@ func TestValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index 4face5a95..b9fe31129 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -4,8 +4,9 @@ package fwschema import ( - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" ) // Attribute is the core interface required for implementing Terraform @@ -63,6 +64,23 @@ type Attribute interface { // sensitive. This is named differently than Sensitive to prevent a // conflict with the tfsdk.Attribute field name. IsSensitive() bool + + // IsWriteOnly should return true if the attribute configuration value is + // write-only. This is named differently than WriteOnly to prevent a + // conflict with the tfsdk.Attribute field name. + // + // Write-only attributes are a managed-resource schema concept only. + IsWriteOnly() bool + + // IsOptionalForImport should return true if the identity attribute is optional to be set by + // the practitioner when importing by identity. This is named differently than OptionalForImport + // to prevent a conflict with the relevant field name. + IsOptionalForImport() bool + + // IsRequiredForImport should return true if the identity attribute must be set by + // the practitioner when importing by identity. This is named differently than RequiredForImport + // to prevent a conflict with the relevant field name. + IsRequiredForImport() bool } // AttributesEqual is a helper function to perform equality testing on two @@ -101,5 +119,17 @@ func AttributesEqual(a, b Attribute) bool { return false } + if a.IsWriteOnly() != b.IsWriteOnly() { + return false + } + + if a.IsOptionalForImport() != b.IsOptionalForImport() { + return false + } + + if a.IsRequiredForImport() != b.IsRequiredForImport() { + return false + } + return true } diff --git a/internal/fwschema/attribute_name_validation_test.go b/internal/fwschema/attribute_name_validation_test.go index accfb52b9..f07d7cfad 100644 --- a/internal/fwschema/attribute_name_validation_test.go +++ b/internal/fwschema/attribute_name_validation_test.go @@ -69,8 +69,6 @@ func TestIsReservedProviderAttributeName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -205,8 +203,6 @@ func TestIsReservedResourceAttributeName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -330,8 +326,6 @@ func TestIsValidAttributeName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschema/diagnostics.go b/internal/fwschema/diagnostics.go index c79531326..efb53c00f 100644 --- a/internal/fwschema/diagnostics.go +++ b/internal/fwschema/diagnostics.go @@ -64,3 +64,19 @@ func AttributeDefaultTypeMismatchDiag(attributePath path.Path, expectedType attr "The default value must match the type of the schema.", ) } + +// AttributeInvalidElementTypeDiag returns an error diagnostic to provider +// developers about using non-primitive types in their Attribute +// implementation. This is not allowed. +func AttributeInvalidElementTypeDiag(attributePath path.Path, actualType attr.Type) diag.Diagnostic { + // The diagnostic path is intentionally omitted as it is invalid in this + // context. Diagnostic paths are intended to be mapped to actual data, + // while this path information must be synthesized. + return diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q contains an element of type %q that is not allowed for Lists in Resource Identity. ", attributePath, actualType)+ + "Lists in Resource Identity may only have primitive element types such as Bool, Int, Float, Number and String.", + ) +} diff --git a/internal/fwschema/schema_test.go b/internal/fwschema/schema_test.go index 89d2ee2a9..0e54405ee 100644 --- a/internal/fwschema/schema_test.go +++ b/internal/fwschema/schema_test.go @@ -170,8 +170,6 @@ func TestSchemaBlockPathExpressions(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -346,8 +344,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -521,8 +517,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschema/write_only_nested_attribute_validation.go b/internal/fwschema/write_only_nested_attribute_validation.go new file mode 100644 index 000000000..bb4812cb2 --- /dev/null +++ b/internal/fwschema/write_only_nested_attribute_validation.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwschema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// ContainsAllWriteOnlyChildAttributes will return true if all child attributes for the +// given nested attribute have WriteOnly set to true. +func ContainsAllWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + if !childAttr.IsWriteOnly() { + return false + } + + nestedAttribute, ok := childAttr.(NestedAttribute) + if ok { + if !ContainsAllWriteOnlyChildAttributes(nestedAttribute) { + return false + } + } + } + + return true +} + +// ContainsAnyWriteOnlyChildAttributes will return true if any child attribute for the +// given nested attribute has WriteOnly set to true. +func ContainsAnyWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + if childAttr.IsWriteOnly() { + return true + } + + nestedAttribute, ok := childAttr.(NestedAttribute) + if ok { + if ContainsAnyWriteOnlyChildAttributes(nestedAttribute) { + return true + } + } + } + + return false +} + +// BlockContainsAnyWriteOnlyChildAttributes will return true if any child attribute for the +// given nested block has WriteOnly set to true. +func BlockContainsAnyWriteOnlyChildAttributes(block Block) bool { + nestedObjAttrs := block.GetNestedObject().GetAttributes() + nestedObjBlocks := block.GetNestedObject().GetBlocks() + + for _, childAttr := range nestedObjAttrs { + if childAttr.IsWriteOnly() { + return true + } + + nestedAttribute, ok := childAttr.(NestedAttribute) + if ok { + if ContainsAnyWriteOnlyChildAttributes(nestedAttribute) { + return true + } + } + } + + for _, childBlock := range nestedObjBlocks { + if BlockContainsAnyWriteOnlyChildAttributes(childBlock) { + return true + } + } + + return false +} + +func InvalidWriteOnlyNestedAttributeDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ) +} + +func InvalidSetNestedAttributeWithWriteOnlyDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a set nested attribute that contains a WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a set nested attribute must have WriteOnly set to false.", + ) +} + +func SetBlockCollectionWithWriteOnlyDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a set nested block that contains a WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute within a set nested block must have WriteOnly set to false.", + ) +} + +func InvalidComputedNestedAttributeWithWriteOnlyDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a Computed nested attribute that contains a WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ) +} diff --git a/internal/fwschemadata/data_default_test.go b/internal/fwschemadata/data_default_test.go index d68208439..054021eb3 100644 --- a/internal/fwschemadata/data_default_test.go +++ b/internal/fwschemadata/data_default_test.go @@ -10389,8 +10389,6 @@ func TestDataDefault(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_description.go b/internal/fwschemadata/data_description.go index 70ae62c76..282a53321 100644 --- a/internal/fwschemadata/data_description.go +++ b/internal/fwschemadata/data_description.go @@ -19,6 +19,10 @@ const ( // DataDescriptionEphemeralResultData is used for Data that represents // the result of an ephemeral operation. DataDescriptionEphemeralResultData DataDescription = "ephemeral result data" + + // DataDescriptionResourceIdentity is used for Data that represents + // a managed resource identity. + DataDescriptionResourceIdentity DataDescription = "resource identity" ) // DataDescription is a human friendly type for Data. Used in error @@ -46,6 +50,8 @@ func (d DataDescription) Title() string { return "State" case DataDescriptionEphemeralResultData: return "Ephemeral Result Data" + case DataDescriptionResourceIdentity: + return "Resource Identity" default: return "Data" } diff --git a/internal/fwschemadata/data_get_at_path_test.go b/internal/fwschemadata/data_get_at_path_test.go index 4bb5b95b3..b1546f635 100644 --- a/internal/fwschemadata/data_get_at_path_test.go +++ b/internal/fwschemadata/data_get_at_path_test.go @@ -7338,7 +7338,6 @@ func TestDataGetAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_get_test.go b/internal/fwschemadata/data_get_test.go index 6528e985e..78d32a4dc 100644 --- a/internal/fwschemadata/data_get_test.go +++ b/internal/fwschemadata/data_get_test.go @@ -8189,7 +8189,6 @@ func TestDataGet(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_nullify_collection_blocks.go b/internal/fwschemadata/data_nullify_collection_blocks.go index f907d6d16..a0a19ecd4 100644 --- a/internal/fwschemadata/data_nullify_collection_blocks.go +++ b/internal/fwschemadata/data_nullify_collection_blocks.go @@ -7,11 +7,12 @@ import ( "context" "errors" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // NullifyCollectionBlocks converts list and set block empty values to null diff --git a/internal/fwschemadata/data_nullify_collection_blocks_test.go b/internal/fwschemadata/data_nullify_collection_blocks_test.go index 9eb2a4c9c..7a2de5f1b 100644 --- a/internal/fwschemadata/data_nullify_collection_blocks_test.go +++ b/internal/fwschemadata/data_nullify_collection_blocks_test.go @@ -950,8 +950,6 @@ func TestDataNullifyCollectionBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_path_exists_test.go b/internal/fwschemadata/data_path_exists_test.go index fd67033f6..b3f685b79 100644 --- a/internal/fwschemadata/data_path_exists_test.go +++ b/internal/fwschemadata/data_path_exists_test.go @@ -799,7 +799,6 @@ func TestDataPathExists(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_path_matches_test.go b/internal/fwschemadata/data_path_matches_test.go index abbe5be3a..b3a307528 100644 --- a/internal/fwschemadata/data_path_matches_test.go +++ b/internal/fwschemadata/data_path_matches_test.go @@ -1469,8 +1469,6 @@ func TestDataPathMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_reify_null_collection_blocks_test.go b/internal/fwschemadata/data_reify_null_collection_blocks_test.go index 1e7161a2f..50d0757a1 100644 --- a/internal/fwschemadata/data_reify_null_collection_blocks_test.go +++ b/internal/fwschemadata/data_reify_null_collection_blocks_test.go @@ -1194,8 +1194,6 @@ func TestDataReifyNullCollectionBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_set_at_path_test.go b/internal/fwschemadata/data_set_at_path_test.go index 13d7e878a..95a41fee9 100644 --- a/internal/fwschemadata/data_set_at_path_test.go +++ b/internal/fwschemadata/data_set_at_path_test.go @@ -3045,7 +3045,6 @@ func TestDataSetAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_set_test.go b/internal/fwschemadata/data_set_test.go index ce827faf9..995923187 100644 --- a/internal/fwschemadata/data_set_test.go +++ b/internal/fwschemadata/data_set_test.go @@ -279,7 +279,6 @@ func TestDataSet(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_valid_path_expression_test.go b/internal/fwschemadata/data_valid_path_expression_test.go index decef1271..13ef1489a 100644 --- a/internal/fwschemadata/data_valid_path_expression_test.go +++ b/internal/fwschemadata/data_valid_path_expression_test.go @@ -284,8 +284,6 @@ func TestDataValidPathExpression(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/data_value_test.go b/internal/fwschemadata/data_value_test.go index 10cdb9afb..a067e19c4 100644 --- a/internal/fwschemadata/data_value_test.go +++ b/internal/fwschemadata/data_value_test.go @@ -2362,7 +2362,6 @@ func TestDataValueAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/tftypes_value_test.go b/internal/fwschemadata/tftypes_value_test.go index 465745523..f9409f4d0 100644 --- a/internal/fwschemadata/tftypes_value_test.go +++ b/internal/fwschemadata/tftypes_value_test.go @@ -154,7 +154,6 @@ func TestCreateParentTerraformValue(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -493,7 +492,6 @@ func TestUpsertChildTerraformValue(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_bool_test.go b/internal/fwschemadata/value_semantic_equality_bool_test.go index 099e6dc68..1468951bf 100644 --- a/internal/fwschemadata/value_semantic_equality_bool_test.go +++ b/internal/fwschemadata/value_semantic_equality_bool_test.go @@ -108,8 +108,6 @@ func TestValueSemanticEqualityBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_dynamic_test.go b/internal/fwschemadata/value_semantic_equality_dynamic_test.go index 183b893c1..2cccd0ab6 100644 --- a/internal/fwschemadata/value_semantic_equality_dynamic_test.go +++ b/internal/fwschemadata/value_semantic_equality_dynamic_test.go @@ -184,8 +184,6 @@ func TestValueSemanticEqualityDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_float32_test.go b/internal/fwschemadata/value_semantic_equality_float32_test.go index 9af96709e..725650d6c 100644 --- a/internal/fwschemadata/value_semantic_equality_float32_test.go +++ b/internal/fwschemadata/value_semantic_equality_float32_test.go @@ -109,8 +109,6 @@ func TestValueSemanticEqualityFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_float64_test.go b/internal/fwschemadata/value_semantic_equality_float64_test.go index 009459118..821485077 100644 --- a/internal/fwschemadata/value_semantic_equality_float64_test.go +++ b/internal/fwschemadata/value_semantic_equality_float64_test.go @@ -108,8 +108,6 @@ func TestValueSemanticEqualityFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_int32_test.go b/internal/fwschemadata/value_semantic_equality_int32_test.go index 4c48b9ae0..ddc2d47a1 100644 --- a/internal/fwschemadata/value_semantic_equality_int32_test.go +++ b/internal/fwschemadata/value_semantic_equality_int32_test.go @@ -109,8 +109,6 @@ func TestValueSemanticEqualityInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_int64_test.go b/internal/fwschemadata/value_semantic_equality_int64_test.go index c8f35f581..f6f29cd4d 100644 --- a/internal/fwschemadata/value_semantic_equality_int64_test.go +++ b/internal/fwschemadata/value_semantic_equality_int64_test.go @@ -108,8 +108,6 @@ func TestValueSemanticEqualityInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_list_test.go b/internal/fwschemadata/value_semantic_equality_list_test.go index 4bb5ba3b6..01220af9e 100644 --- a/internal/fwschemadata/value_semantic_equality_list_test.go +++ b/internal/fwschemadata/value_semantic_equality_list_test.go @@ -638,8 +638,6 @@ func TestValueSemanticEqualityList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_map_test.go b/internal/fwschemadata/value_semantic_equality_map_test.go index f2c33edf0..01e163d2a 100644 --- a/internal/fwschemadata/value_semantic_equality_map_test.go +++ b/internal/fwschemadata/value_semantic_equality_map_test.go @@ -562,8 +562,6 @@ func TestValueSemanticEqualityMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_number_test.go b/internal/fwschemadata/value_semantic_equality_number_test.go index f912e09c4..060b959d1 100644 --- a/internal/fwschemadata/value_semantic_equality_number_test.go +++ b/internal/fwschemadata/value_semantic_equality_number_test.go @@ -109,8 +109,6 @@ func TestValueSemanticEqualityNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_object_test.go b/internal/fwschemadata/value_semantic_equality_object_test.go index c68f55638..fb1b1af84 100644 --- a/internal/fwschemadata/value_semantic_equality_object_test.go +++ b/internal/fwschemadata/value_semantic_equality_object_test.go @@ -658,8 +658,6 @@ func TestValueSemanticEqualityObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_set.go b/internal/fwschemadata/value_semantic_equality_set.go index 1afe626f4..4ad2ea946 100644 --- a/internal/fwschemadata/value_semantic_equality_set.go +++ b/internal/fwschemadata/value_semantic_equality_set.go @@ -136,33 +136,40 @@ func ValueSemanticEqualitySetElements(ctx context.Context, req ValueSemanticEqua // Ensure new value always contains all of proposed new value newValueElements[idx] = proposedNewValueElement - if idx >= len(priorValueElements) { - continue + // Loop through all prior value elements and see if there are any semantically equal elements + for pIdx, priorValueElement := range priorValueElements { + elementReq := ValueSemanticEqualityRequest{ + Path: req.Path.AtSetValue(proposedNewValueElement), + PriorValue: priorValueElement, + ProposedNewValue: proposedNewValueElement, + } + elementResp := &ValueSemanticEqualityResponse{ + NewValue: elementReq.ProposedNewValue, + } + + ValueSemanticEquality(ctx, elementReq, elementResp) + + resp.Diagnostics.Append(elementResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + + if elementResp.NewValue.Equal(elementReq.ProposedNewValue) { + // This prior value element didn't match, but there could be other elements that do + continue + } + + // Prior state was kept, meaning that we found a semantically equal element + updatedElements = true + + // Remove the semantically equal element from the slice of candidates + priorValueElements = append(priorValueElements[:pIdx], priorValueElements[pIdx+1:]...) + + // Order doesn't matter, so we can just set the prior state element to this index + newValueElements[idx] = elementResp.NewValue + break } - - elementReq := ValueSemanticEqualityRequest{ - Path: req.Path.AtSetValue(proposedNewValueElement), - PriorValue: priorValueElements[idx], - ProposedNewValue: proposedNewValueElement, - } - elementResp := &ValueSemanticEqualityResponse{ - NewValue: elementReq.ProposedNewValue, - } - - ValueSemanticEquality(ctx, elementReq, elementResp) - - resp.Diagnostics.Append(elementResp.Diagnostics...) - - if resp.Diagnostics.HasError() { - return - } - - if elementResp.NewValue.Equal(elementReq.ProposedNewValue) { - continue - } - - updatedElements = true - newValueElements[idx] = elementResp.NewValue } // No changes required if the elements were not updated. diff --git a/internal/fwschemadata/value_semantic_equality_set_test.go b/internal/fwschemadata/value_semantic_equality_set_test.go index 59b34856b..0108eaddc 100644 --- a/internal/fwschemadata/value_semantic_equality_set_test.go +++ b/internal/fwschemadata/value_semantic_equality_set_test.go @@ -50,6 +50,34 @@ func TestValueSemanticEqualitySet(t *testing.T) { ), }, }, + "SetValue-diff-order": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("prior"), + types.StringValue("value"), + }, + ), + ProposedNewValue: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("value"), + types.StringValue("new"), + }, + ), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("value"), + types.StringValue("new"), + }, + ), + }, + }, // ElementType with semantic equality "SetValue-StringValuableWithSemanticEquals-true": { request: fwschemadata.ValueSemanticEqualityRequest{ @@ -91,6 +119,64 @@ func TestValueSemanticEqualitySet(t *testing.T) { ), }, }, + "SetValue-StringValuableWithSemanticEquals-true-diff-order": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + }, + }, + }, + ), + ProposedNewValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + }, + }, + }, + ), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + }, + }, + }, + ), + }, + }, "SetValue-StringValuableWithSemanticEquals-false": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), @@ -131,6 +217,58 @@ func TestValueSemanticEqualitySet(t *testing.T) { ), }, }, + "SetValue-StringValuableWithSemanticEquals-false-diff-order": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("prior"), + SemanticEquals: false, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + }, + ), + ProposedNewValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("new"), + SemanticEquals: false, + }, + }, + ), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("new"), + SemanticEquals: false, + }, + }, + ), + }, + }, "SetValue-StringValuableWithSemanticEquals-diagnostics": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), @@ -267,6 +405,136 @@ func TestValueSemanticEqualitySet(t *testing.T) { ), }, }, + "SetValue-SetValue-StringValuableWithSemanticEquals-true-diff-order": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{}, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + }, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-789"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-789"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-012"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-012"), + }, + }, + }, + ), + }, + ), + ProposedNewValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{}, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-012"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-012"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-789"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-789"), + }, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + }, + }, + }, + ), + }, + ), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{}, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-123"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-123"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-456"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-456"), + }, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{}, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-789"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-789"), + }, + }, + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("keep-lowercase-012"), + SemanticallyEqualTo: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("KEEP-LOWERCASE-012"), + }, + }, + }, + ), + }, + ), + }, + }, "SetValue-SetValue-StringValuableWithSemanticEquals-false": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), @@ -334,6 +602,106 @@ func TestValueSemanticEqualitySet(t *testing.T) { ), }, }, + "SetValue-SetValue-StringValuableWithSemanticEquals-false-diff-order": { + request: fwschemadata.ValueSemanticEqualityRequest{ + Path: path.Root("test"), + PriorValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("prior"), + SemanticEquals: false, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + }, + ), + }, + ), + ProposedNewValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("new"), + SemanticEquals: false, + }, + }, + ), + }, + ), + }, + expected: &fwschemadata.ValueSemanticEqualityResponse{ + NewValue: types.SetValueMust( + types.SetType{ + ElemType: testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + }, + []attr.Value{ + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("value"), + SemanticEquals: false, + }, + }, + ), + types.SetValueMust( + testtypes.StringTypeWithSemanticEquals{ + SemanticEquals: false, + }, + []attr.Value{ + testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("new"), + SemanticEquals: false, + }, + }, + ), + }, + ), + }, + }, "SetValue-SetValue-StringValuableWithSemanticEquals-NewValueElementsGreaterThanPriorValueElements": { request: fwschemadata.ValueSemanticEqualityRequest{ Path: path.Root("test"), @@ -638,8 +1006,6 @@ func TestValueSemanticEqualitySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_string_test.go b/internal/fwschemadata/value_semantic_equality_string_test.go index d705e9fbd..7e730cbe3 100644 --- a/internal/fwschemadata/value_semantic_equality_string_test.go +++ b/internal/fwschemadata/value_semantic_equality_string_test.go @@ -108,8 +108,6 @@ func TestValueSemanticEqualityString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwschemadata/value_semantic_equality_test.go b/internal/fwschemadata/value_semantic_equality_test.go index 81c85e335..64fe80962 100644 --- a/internal/fwschemadata/value_semantic_equality_test.go +++ b/internal/fwschemadata/value_semantic_equality_test.go @@ -1205,8 +1205,6 @@ func TestValueSemanticEquality(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index f390311a9..a5c717964 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -3288,7 +3288,6 @@ func TestAttributeModifyPlan(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -3929,8 +3928,6 @@ func TestAttributePlanModifyBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -4565,8 +4562,6 @@ func TestAttributePlanModifyFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -5201,8 +5196,6 @@ func TestAttributePlanModifyFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -5837,8 +5830,6 @@ func TestAttributePlanModifyInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -6473,8 +6464,6 @@ func TestAttributePlanModifyInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -7127,8 +7116,6 @@ func TestAttributePlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -8108,8 +8095,6 @@ func TestAttributePlanModifyMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -8744,8 +8729,6 @@ func TestAttributePlanModifyNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -9943,8 +9926,6 @@ func TestAttributePlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -10597,8 +10578,6 @@ func TestAttributePlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -11233,8 +11212,6 @@ func TestAttributePlanModifyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -11869,8 +11846,6 @@ func TestAttributePlanModifyDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -13031,8 +13006,6 @@ func TestNestedAttributeObjectPlanModify(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 98b05f0fb..e040551ec 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -34,6 +34,11 @@ type ValidateAttributeRequest struct { // Config contains the entire configuration of the data source, provider, or resource. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateAttributeResponse represents a response to a @@ -65,6 +70,24 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt return } + if a.IsWriteOnly() && a.IsRequired() && a.IsOptional() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ) + return + } + + if a.IsWriteOnly() && a.IsComputed() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ) + return + } + configData := &fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, Schema: req.Config.Schema, @@ -78,12 +101,30 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt return } + configHasNullValue := attributeConfig.IsNull() + configHasUnknownValue := attributeConfig.IsUnknown() + // If the value is dynamic, we still need to check if the underlying value is null or unknown + if dynamicValuable, isDynamic := attributeConfig.(basetypes.DynamicValuable); !configHasNullValue && !configHasUnknownValue && isDynamic { + dynamicConfigVal, diags := dynamicValuable.ToDynamicValue(ctx) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + if dynamicConfigVal.IsUnderlyingValueNull() { + configHasNullValue = true + } + + if dynamicConfigVal.IsUnderlyingValueUnknown() { + configHasUnknownValue = true + } + } + // Terraform CLI does not automatically perform certain configuration // checks yet. If it eventually does, this logic should remain at least // until Terraform CLI versions 0.12 through the release containing the // checks are considered end-of-life. // Reference: https://github.com/hashicorp/terraform/issues/30669 - if a.IsComputed() && !a.IsOptional() && !attributeConfig.IsNull() { + if a.IsComputed() && !a.IsOptional() && !configHasNullValue { resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Configuration for Read-Only Attribute", @@ -97,7 +138,7 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt // until Terraform CLI versions 0.12 through the release containing the // checks are considered end-of-life. // Reference: https://github.com/hashicorp/terraform/issues/30669 - if a.IsRequired() && attributeConfig.IsNull() { + if a.IsRequired() && configHasNullValue { resp.Diagnostics.AddAttributeError( req.AttributePath, "Missing Configuration for Required Attribute", @@ -106,6 +147,18 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt ) } + // If the client doesn't support write-only attributes (first supported in Terraform v1.11.0), then we raise an early validation error + // to avoid a confusing data consistency error when the provider attempts to return "null" for a write-only attribute in the planned/final state. + // + // Write-only attributes can only be successfully used with a supporting client, so the only option for a practitoner to utilize a write-only attribute + // is to upgrade their Terraform CLI version to v1.11.0 or later. + if !req.ClientCapabilities.WriteOnlyAttributesAllowed && a.IsWriteOnly() && !configHasNullValue { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "WriteOnly Attribute Not Allowed", + fmt.Sprintf("The resource contains a non-null value for WriteOnly attribute %s. Write-only attributes are only supported in Terraform 1.11 and later.", req.AttributePath.String()), + ) + } req.AttributeConfig = attributeConfig switch attributeWithValidators := a.(type) { @@ -138,33 +191,13 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt AttributeValidateNestedAttributes(ctx, a, req, resp) // Show deprecation warnings only for known values. - if a.GetDeprecationMessage() != "" && !attributeConfig.IsNull() && !attributeConfig.IsUnknown() { - // Dynamic values need to perform more logic to check the config value for null/unknown-ness - dynamicValuable, ok := attributeConfig.(basetypes.DynamicValuable) - if !ok { - resp.Diagnostics.AddAttributeWarning( - req.AttributePath, - "Attribute Deprecated", - a.GetDeprecationMessage(), - ) - return - } - - dynamicConfigVal, diags := dynamicValuable.ToDynamicValue(ctx) - resp.Diagnostics.Append(diags...) - if diags.HasError() { - return - } - - // For dynamic values, it's possible to be known when only the type is known. - // The underlying value can still be null or unknown, so check for that here - if !dynamicConfigVal.IsUnderlyingValueNull() && !dynamicConfigVal.IsUnderlyingValueUnknown() { - resp.Diagnostics.AddAttributeWarning( - req.AttributePath, - "Attribute Deprecated", - a.GetDeprecationMessage(), - ) - } + if a.GetDeprecationMessage() != "" && !configHasNullValue && !configHasUnknownValue { + resp.Diagnostics.AddAttributeWarning( + req.AttributePath, + "Attribute Deprecated", + a.GetDeprecationMessage(), + ) + return } } @@ -200,10 +233,11 @@ func AttributeValidateBool(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.BoolRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.BoolValidators() { @@ -265,10 +299,11 @@ func AttributeValidateFloat32(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float32Validators() { @@ -330,10 +365,11 @@ func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float64Validators() { @@ -395,10 +431,11 @@ func AttributeValidateInt32(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int32Validators() { @@ -460,10 +497,11 @@ func AttributeValidateInt64(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int64Validators() { @@ -525,10 +563,11 @@ func AttributeValidateList(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ListValidators() { @@ -590,10 +629,11 @@ func AttributeValidateMap(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.MapRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.MapValidators() { @@ -655,10 +695,11 @@ func AttributeValidateNumber(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.NumberRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.NumberValidators() { @@ -720,10 +761,11 @@ func AttributeValidateObject(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ObjectValidators() { @@ -785,10 +827,11 @@ func AttributeValidateSet(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.SetValidators() { @@ -850,10 +893,11 @@ func AttributeValidateString(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.StringRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.StringValidators() { @@ -915,10 +959,11 @@ func AttributeValidateDynamic(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.DynamicRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.DynamicValidators() { @@ -992,6 +1037,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1026,6 +1072,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1060,6 +1107,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtMapKey(key), AttributePathExpression: req.AttributePathExpression.AtMapKey(key), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1097,6 +1145,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1144,10 +1193,11 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { @@ -1182,6 +1232,7 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttrResp := &ValidateAttributeResponse{} diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 457264e0f..04473e359 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -181,6 +181,29 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "config-computed-dynamic-underlying-null-value": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.DynamicPseudoType, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Computed: true, + Type: types.DynamicType, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "config-optional-computed-null": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -331,6 +354,38 @@ func TestAttributeValidate(t *testing.T) { }, resp: ValidateAttributeResponse{}, }, + "config-required-dynamic-underlying-null-value": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.DynamicPseudoType, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Required: true, + Type: types.DynamicType, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Configuration for Required Attribute", + "Must set a configuration value for the test attribute as the provider has marked it as required.\n\n"+ + "Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.", + ), + }, + }, + }, "no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -1700,10 +1755,314 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "write-only-attr-with-required": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-required-null-value": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Configuration for Required Attribute", + "Must set a configuration value for the test attribute as the provider has marked it as required.\n\n"+ + "Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.", + ), + }, + }, + }, + "write-only-attr-with-required-dynamic-underlying-null-value": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.DynamicPseudoType, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.DynamicType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Configuration for Required Attribute", + "Must set a configuration value for the test attribute as the provider has marked it as required.\n\n"+ + "Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.", + ), + }, + }, + }, + "write-only-attr-with-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-computed": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-missing-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "Attribute missing Required, Optional, or Computed definition. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-computed-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-set-no-client-capability": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + // Client indicating it doesn't support write-only attributes + WriteOnlyAttributesAllowed: false, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "hello world!"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Required: true, + WriteOnly: true, + Type: types.StringType, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "WriteOnly Attribute Not Allowed", + "The resource contains a non-null value for WriteOnly attribute test. "+ + "Write-only attributes are only supported in Terraform 1.11 and later.", + ), + }, + }, + }, } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -1786,6 +2145,32 @@ func TestAttributeValidateBool(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithBoolValidators{ Validators: []validator.Bool{ @@ -1916,8 +2301,6 @@ func TestAttributeValidateBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1990,6 +2373,32 @@ func TestAttributeValidateFloat32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(0.1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat32Validators{ Validators: []validator.Float32{ @@ -2120,8 +2529,6 @@ func TestAttributeValidateFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2194,6 +2601,32 @@ func TestAttributeValidateFloat64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(0.2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat64Validators{ Validators: []validator.Float64{ @@ -2324,8 +2757,6 @@ func TestAttributeValidateFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2398,6 +2829,32 @@ func TestAttributeValidateInt32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt32Validators{ Validators: []validator.Int32{ @@ -2528,8 +2985,6 @@ func TestAttributeValidateInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2602,6 +3057,32 @@ func TestAttributeValidateInt64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt64Validators{ Validators: []validator.Int64{ @@ -2732,8 +3213,6 @@ func TestAttributeValidateInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2808,6 +3287,32 @@ func TestAttributeValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithListValidators{ + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithListValidators{ ElementType: types.StringType, @@ -2951,8 +3456,6 @@ func TestAttributeValidateList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3033,6 +3536,35 @@ func TestAttributeValidateMap(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithMapValidators{ + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected MapRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithMapValidators{ ElementType: types.StringType, @@ -3188,8 +3720,6 @@ func TestAttributeValidateMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3262,6 +3792,32 @@ func TestAttributeValidateNumber(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithNumberValidators{ Validators: []validator.Number{ @@ -3392,8 +3948,6 @@ func TestAttributeValidateNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3478,6 +4032,35 @@ func TestAttributeValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithObjectValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithObjectValidators{ AttributeTypes: map[string]attr.Type{ @@ -3639,8 +4222,6 @@ func TestAttributeValidateObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3715,6 +4296,32 @@ func TestAttributeValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithSetValidators{ + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithSetValidators{ ElementType: types.StringType, @@ -3858,8 +4465,6 @@ func TestAttributeValidateSet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -3932,6 +4537,32 @@ func TestAttributeValidateString(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testVal"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithStringValidators{ Validators: []validator.String{ @@ -4062,8 +4693,6 @@ func TestAttributeValidateString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -4136,6 +4765,32 @@ func TestAttributeValidateDynamic(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithDynamicValidators{ + Validators: []validator.Dynamic{ + testvalidator.Dynamic{ + ValidateDynamicMethod: func(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected DynamicRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.DynamicValue(types.StringValue("test")), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithDynamicValidators{ Validators: []validator.Dynamic{ @@ -4266,8 +4921,6 @@ func TestAttributeValidateDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -4373,6 +5026,32 @@ func TestNestedAttributeObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { object: testschema.NestedAttributeObjectWithValidators{ Validators: []validator.Object{ @@ -4520,8 +5199,6 @@ func TestNestedAttributeObjectValidateObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 323ca8709..53f18579f 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -4024,7 +4024,6 @@ func TestBlockModifyPlan(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -4685,8 +4684,6 @@ func TestBlockPlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -5884,8 +5881,6 @@ func TestBlockPlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -6538,8 +6533,6 @@ func TestBlockPlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -7755,8 +7748,6 @@ func TestNestedBlockObjectPlanModify(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index d65b1b5c7..fcf3898f8 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -76,6 +76,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -110,6 +111,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -143,6 +145,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: o, AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -203,10 +206,11 @@ func BlockValidateList(ctx context.Context, block fwxschema.BlockWithListValidat } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ListValidators() { @@ -268,10 +272,11 @@ func BlockValidateObject(ctx context.Context, block fwxschema.BlockWithObjectVal } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ObjectValidators() { @@ -333,10 +338,11 @@ func BlockValidateSet(ctx context.Context, block fwxschema.BlockWithSetValidator } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.SetValidators() { @@ -395,10 +401,11 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { @@ -432,6 +439,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedAttrReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedAttrResp := &ValidateAttributeResponse{} @@ -445,6 +453,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedBlockReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockResp := &ValidateAttributeResponse{} diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index 367a0df1c..2c6f3981d 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -20,7 +22,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBlockValidate(t *testing.T) { @@ -746,6 +747,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "list-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeList, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "set-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -874,6 +946,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "set-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSet, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "single-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -976,10 +1119,67 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "single-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -1085,6 +1285,44 @@ func TestBlockValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithListValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1316,8 +1554,6 @@ func TestBlockValidateList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1402,6 +1638,37 @@ func TestBlockValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithObjectValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1579,8 +1846,6 @@ func TestBlockValidateObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1679,6 +1944,44 @@ func TestBlockValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithSetValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1910,8 +2213,6 @@ func TestBlockValidateSet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2069,6 +2370,31 @@ func TestNestedBlockObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { object: testschema.NestedBlockObjectWithValidators{ Validators: []validator.Object{ @@ -2252,8 +2578,6 @@ func TestNestedBlockObjectValidateObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/schema_plan_modification_test.go b/internal/fwserver/schema_plan_modification_test.go index 3e00223ec..a3bc0ab5f 100644 --- a/internal/fwserver/schema_plan_modification_test.go +++ b/internal/fwserver/schema_plan_modification_test.go @@ -2425,7 +2425,6 @@ func TestSchemaModifyPlan(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/schema_semantic_equality_test.go b/internal/fwserver/schema_semantic_equality_test.go index b9ab45d92..65018998f 100644 --- a/internal/fwserver/schema_semantic_equality_test.go +++ b/internal/fwserver/schema_semantic_equality_test.go @@ -2300,8 +2300,6 @@ func TestSchemaSemanticEquality(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/schema_validation.go b/internal/fwserver/schema_validation.go index 32c50f9d4..dedce9d43 100644 --- a/internal/fwserver/schema_validation.go +++ b/internal/fwserver/schema_validation.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -20,6 +21,11 @@ type ValidateSchemaRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateSchemaResponse represents a response to a @@ -43,6 +49,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. @@ -58,6 +65,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/schema_validation_test.go b/internal/fwserver/schema_validation_test.go index f896c3e08..81b0d4d37 100644 --- a/internal/fwserver/schema_validation_test.go +++ b/internal/fwserver/schema_validation_test.go @@ -172,7 +172,6 @@ func TestSchemaValidate(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b6f2bc85e..b4b8a779b 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -158,6 +158,15 @@ type Server struct { // access from race conditions. resourceSchemasMutex sync.RWMutex + // resourceIdentitySchemas is the cached Resource Identity Schemas for RPCs that need to + // convert resource identity data from the protocol. If not found, it will be + // fetched from the Resource IdentitySchema method. + resourceIdentitySchemas map[string]fwschema.Schema + + // resourceIdentitySchemasMutex is a mutex to protect concurrent resourceIdentitySchemas + // access from race conditions. + resourceIdentitySchemasMutex sync.RWMutex + // resourceFuncs is the cached Resource functions for RPCs that need to // access resources. If not found, it will be fetched from the // Provider.Resources() method. @@ -689,3 +698,109 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem return resourceSchemas, diags } + +// ResourceIdentitySchema returns the Resource Identity Schema for the given type name and +// caches the result for later Identity operations. Identity is an optional feature for resources, +// so this function will return a nil schema with no diagnostics if the resource type doesn't define +// an identity schema. +func (s *Server) ResourceIdentitySchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { + s.resourceIdentitySchemasMutex.RLock() + resourceIdentitySchema, ok := s.resourceIdentitySchemas[typeName] + s.resourceIdentitySchemasMutex.RUnlock() + + if ok { + return resourceIdentitySchema, nil + } + + var diags diag.Diagnostics + + r, resourceDiags := s.Resource(ctx, typeName) + + diags.Append(resourceDiags...) + + if diags.HasError() { + return nil, diags + } + + resourceWithIdentity, ok := r.(resource.ResourceWithIdentity) + if !ok { + // It's valid for a resource to not have an identity, so cache and return a nil schema + s.resourceIdentitySchemasMutex.Lock() + if s.resourceIdentitySchemas == nil { + s.resourceIdentitySchemas = make(map[string]fwschema.Schema) + } + + s.resourceIdentitySchemas[typeName] = nil + s.resourceIdentitySchemasMutex.Unlock() + + return nil, nil + } + + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := resource.IdentitySchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + resourceWithIdentity.IdentitySchema(ctx, identitySchemaReq, &identitySchemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + + diags.Append(identitySchemaResp.Diagnostics...) + + if diags.HasError() { + return identitySchemaResp.IdentitySchema, diags + } + + s.resourceIdentitySchemasMutex.Lock() + if s.resourceIdentitySchemas == nil { + s.resourceIdentitySchemas = make(map[string]fwschema.Schema) + } + + s.resourceIdentitySchemas[typeName] = identitySchemaResp.IdentitySchema + s.resourceIdentitySchemasMutex.Unlock() + + return identitySchemaResp.IdentitySchema, diags +} + +// ResourceIdentitySchemas returns a map of Resource Identity Schemas for the +// GetResourceIdentitySchemas RPC without caching since not all schemas are guaranteed to +// be necessary for later provider operations. The schema implementations are +// also validated. +func (s *Server) ResourceIdentitySchemas(ctx context.Context) (map[string]fwschema.Schema, diag.Diagnostics) { + resourceIdentitySchemas := make(map[string]fwschema.Schema) + + resourceFuncs, diags := s.ResourceFuncs(ctx) + + for typeName, resourceFunc := range resourceFuncs { + r := resourceFunc() + + rWithIdentity, ok := r.(resource.ResourceWithIdentity) + if !ok { + // Resource identity support is optional, so we can skip resources that don't implement it. + continue + } + + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := resource.IdentitySchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + rWithIdentity.IdentitySchema(ctx, identitySchemaReq, &identitySchemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + + diags.Append(identitySchemaResp.Diagnostics...) + + if identitySchemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := identitySchemaResp.IdentitySchema.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + resourceIdentitySchemas[typeName] = identitySchemaResp.IdentitySchema + } + + return resourceIdentitySchemas, diags +} diff --git a/internal/fwserver/server_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index a11a72e47..aabbeeba7 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -17,13 +17,15 @@ import ( // ApplyResourceChangeRequest is the framework server request for the // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // ApplyResourceChangeResponse is the framework server response for the @@ -31,6 +33,7 @@ type ApplyResourceChangeRequest struct { type ApplyResourceChangeResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -45,12 +48,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange received no PriorState, running CreateResource") createReq := &CreateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PlannedIdentity: req.PlannedIdentity, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, + Resource: req.Resource, } createResp := &CreateResourceResponse{} @@ -58,6 +63,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = createResp.Diagnostics resp.NewState = createResp.NewState + resp.NewIdentity = createResp.NewIdentity resp.Private = createResp.Private return @@ -70,8 +76,13 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan deleteReq := &DeleteResourceRequest{ PlannedPrivate: req.PlannedPrivate, PriorState: req.PriorState, + // MAINTAINER NOTE: There isn't a separate data field for prior identity, like there is with prior_state and planned_state. + // Here the planned_identity field will contain what would be considered the prior identity (since the final identity value + // after deleting will be null). + PriorIdentity: req.PlannedIdentity, ProviderMeta: req.ProviderMeta, ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, Resource: req.Resource, } deleteResp := &DeleteResourceResponse{} @@ -80,6 +91,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = deleteResp.Diagnostics resp.NewState = deleteResp.NewState + resp.NewIdentity = deleteResp.NewIdentity resp.Private = deleteResp.Private return @@ -89,13 +101,15 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange running UpdateResource") updateReq := &UpdateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - PriorState: req.PriorState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PlannedIdentity: req.PlannedIdentity, + PriorState: req.PriorState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, + Resource: req.Resource, } updateResp := &UpdateResourceResponse{} @@ -103,5 +117,6 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = updateResp.Diagnostics resp.NewState = updateResp.NewState + resp.NewIdentity = updateResp.NewIdentity resp.Private = updateResp.Private } diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 59e10d76c..0fa2d9ab9 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,12 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +51,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testEmptyPlan := &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -54,11 +69,45 @@ func TestServerApplyResourceChange(t *testing.T) { Schema: testSchema, } + testEmptyIdentity := &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional_write_only": tftypes.String, + "test_required_write_only": tftypes.String, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_optional_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required_write_only": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + } + + type testSchemaDataWriteOnly struct { + TestOptionalWriteOnly types.String `tfsdk:"test_optional_write_only"` + TestRequiredWriteOnly types.String `tfsdk:"test_required_write_only"` + } + + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -218,6 +267,69 @@ func TestServerApplyResourceChange(t *testing.T) { }, Private: testEmptyPrivate}, }, + "create-request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: testEmptyState, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate}, + }, "create-request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -351,6 +463,67 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "create-response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -398,6 +571,53 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newstate-write-only": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "create-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -474,6 +694,52 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: testEmptyState, }, }, + "delete-request-prioridentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testEmptyIdentity, + NewState: testEmptyState, + }, + }, "delete-request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -754,6 +1020,52 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: testEmptyState, }, }, + "delete-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // The identity is automatically set to null in the response after the Delete method is called + if resp.Identity == nil || resp.Identity.Raw.IsNull() { + resp.Diagnostics.AddError( + "Unexpected resp.Identity", + "expected resp.Identity to be a known non-null object of the schema type.", + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testEmptyIdentity, + NewState: testEmptyState, + }, + }, "update-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -868,6 +1180,77 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "update-request-priorstate": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1208,6 +1591,67 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "update-response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1259,6 +1703,59 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "old-optional-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "update-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1353,8 +1850,6 @@ func TestServerApplyResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_callfunction_test.go b/internal/fwserver/server_callfunction_test.go index dfb3ee1cb..e867c9069 100644 --- a/internal/fwserver/server_callfunction_test.go +++ b/internal/fwserver/server_callfunction_test.go @@ -380,8 +380,6 @@ func TestServerCallFunction(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_closeephemeralresource_test.go b/internal/fwserver/server_closeephemeralresource_test.go index 4bad0e620..3ffc19c86 100644 --- a/internal/fwserver/server_closeephemeralresource_test.go +++ b/internal/fwserver/server_closeephemeralresource_test.go @@ -185,8 +185,6 @@ func TestServerCloseEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index f8dd1e9e0..96d5ac63a 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -230,8 +230,6 @@ func TestServerConfigureProvider(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 30c491690..00a13d68e 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -20,12 +20,14 @@ import ( // CreateResourceRequest is the framework server request for a create request // with the ApplyResourceChange RPC. type CreateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // CreateResourceResponse is the framework server response for a create request @@ -33,6 +35,7 @@ type CreateResourceRequest struct { type CreateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -97,12 +100,36 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, createReq.ProviderMeta = *req.ProviderMeta } + // If the resource supports identity and there is no planned identity data, pre-populate with a null value. + if req.PlannedIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Pre-populate the new identity with the planned identity. + if req.PlannedIdentity != nil { + createReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + + createResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Create") req.Resource.Create(ctx, createReq, &createResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Create") resp.Diagnostics = createResp.Diagnostics resp.NewState = &createResp.State + resp.NewIdentity = createResp.Identity if !resp.Diagnostics.HasError() && createResp.State.Raw.Equal(nullSchemaData) { detail := "The Terraform Provider unexpectedly returned no resource state after having no errors in the resource creation. " + @@ -132,6 +159,16 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Create Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider create operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, @@ -156,11 +193,21 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index d3e83c31b..3095638ff 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,19 @@ func TestServerCreateResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +58,14 @@ func TestServerCreateResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -76,6 +98,18 @@ func TestServerCreateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -86,11 +120,20 @@ func TestServerCreateResource(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataWithSemanticEquals struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -220,6 +263,63 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -433,6 +533,110 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Create Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider create operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -506,6 +710,39 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -548,8 +785,6 @@ func TestServerCreateResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_deleteresource.go b/internal/fwserver/server_deleteresource.go index 5879b6706..d58e758d0 100644 --- a/internal/fwserver/server_deleteresource.go +++ b/internal/fwserver/server_deleteresource.go @@ -21,8 +21,10 @@ import ( type DeleteResourceRequest struct { PlannedPrivate *privatestate.Data PriorState *tfsdk.State + PriorIdentity *tfsdk.ResourceIdentity ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema Resource resource.Resource } @@ -31,6 +33,7 @@ type DeleteResourceRequest struct { type DeleteResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -96,6 +99,27 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, resp.Private = req.PlannedPrivate } + if req.PriorIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PriorIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + if req.PriorIdentity != nil { + deleteReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PriorIdentity.Schema, + Raw: req.PriorIdentity.Raw.Copy(), + } + + deleteResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PriorIdentity.Schema, + Raw: req.PriorIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Delete") req.Resource.Delete(ctx, deleteReq, &deleteResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Delete") @@ -108,10 +132,21 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/863 deleteResp.Private = nil resp.Private = nil + + // If the resource supports identity send a null value. + if req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + deleteResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } } resp.Diagnostics = deleteResp.Diagnostics resp.NewState = &deleteResp.State + resp.NewIdentity = deleteResp.Identity if deleteResp.Private != nil { if resp.Private == nil { @@ -120,4 +155,14 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, resp.Private.Provider = deleteResp.Private } + + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Delete Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider delete operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } } diff --git a/internal/fwserver/server_deleteresource_test.go b/internal/fwserver/server_deleteresource_test.go index 95024d9a1..4770f45e9 100644 --- a/internal/fwserver/server_deleteresource_test.go +++ b/internal/fwserver/server_deleteresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,12 @@ func TestServerDeleteResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,16 +51,33 @@ func TestServerDeleteResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, } + testEmptyIdentity := &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -144,6 +168,45 @@ func TestServerDeleteResource(t *testing.T) { NewState: testEmptyState, }, }, + "request-prioridentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + }, + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewState: testEmptyState, + NewIdentity: testEmptyIdentity, + }, + }, "request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -334,6 +397,121 @@ func TestServerDeleteResource(t *testing.T) { NewState: testEmptyState, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // The identity is automatically set to null in the response after the Delete method is called + if resp.Identity == nil || resp.Identity.Raw.IsNull() { + resp.Diagnostics.AddError( + "Unexpected resp.Identity", + "expected resp.Identity to be a known non-null object of the schema type.", + ) + } + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewIdentity: testEmptyIdentity, + NewState: testEmptyState, + }, + }, + "response-newidentity-set-to-null": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // This should be nulled out + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewIdentity: testEmptyIdentity, + NewState: testEmptyState, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // This should raise a diagnostic + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + }, + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Delete Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider delete operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: testEmptyState, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -375,7 +553,7 @@ func TestServerDeleteResource(t *testing.T) { Private: testPrivateProvider, }, }, - "response-private-updated": { + "response-private-Deleted": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -420,8 +598,6 @@ func TestServerDeleteResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_getfunctions_test.go b/internal/fwserver/server_getfunctions_test.go index 1df383710..1713183c6 100644 --- a/internal/fwserver/server_getfunctions_test.go +++ b/internal/fwserver/server_getfunctions_test.go @@ -199,8 +199,6 @@ func TestServerGetFunctions(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 532765e1a..8729f6dbd 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -647,8 +647,6 @@ func TestServerGetMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 43a0c9a4e..e5e17bc89 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -1223,8 +1223,6 @@ func TestServerGetProviderSchema(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_getresourceidentityschemas.go b/internal/fwserver/server_getresourceidentityschemas.go new file mode 100644 index 000000000..511c6bb05 --- /dev/null +++ b/internal/fwserver/server_getresourceidentityschemas.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// GetResourceIdentitySchemasRequest is the framework server request for the +// GetResourceIdentitySchemas RPC. +type GetResourceIdentitySchemasRequest struct{} + +// GetResourceIdentitySchemasResponse is the framework server response for the +// GetResourceIdentitySchemas RPC. +type GetResourceIdentitySchemasResponse struct { + IdentitySchemas map[string]fwschema.Schema + Diagnostics diag.Diagnostics +} + +// GetResourceIdentitySchemas implements the framework server GetResourceIdentitySchemas RPC. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, req *GetResourceIdentitySchemasRequest, resp *GetResourceIdentitySchemasResponse) { + identitySchemas, diags := s.ResourceIdentitySchemas(ctx) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.IdentitySchemas = identitySchemas +} diff --git a/internal/fwserver/server_getresourceidentityschemas_test.go b/internal/fwserver/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..c780a7ff1 --- /dev/null +++ b/internal/fwserver/server_getresourceidentityschemas_test.go @@ -0,0 +1,229 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.GetResourceIdentitySchemasRequest + expectedResponse *fwserver.GetResourceIdentitySchemasResponse + }{ + "empty-provider": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{}, + }, + }, + "resource-no-identity-schemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{}, + }, + }, + "resource-identity-schemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource3" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource4" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource4": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-schemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "$": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource3" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource4" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.GetResourceIdentitySchemasResponse{} + testCase.server.GetResourceIdentitySchemas(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 7288cc3e7..000a0fad6 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -18,14 +19,29 @@ import ( // ImportedResource represents a resource that was imported. type ImportedResource struct { Private *privatestate.Data + Identity *tfsdk.ResourceIdentity State tfsdk.State TypeName string } // ImportResourceStateRequest is the framework server request for the // ImportResourceState RPC. +// +// Either ID or Identity will be supplied depending on how the resource is being imported. type ImportResourceStateRequest struct { - ID string + // ID will come from the import CLI command or an import config block with the "id" attribute assigned. + // + // This ID field is a special string identifier that can be parsed however the provider deems fit. + ID string + + // Identity will come from an import config block with the "identity" attribute assigned and will conform + // to the identity schema defined by the resource. (Terraform v1.12+) + // + // All attributes marked as RequiredForImport will be populated (enforced by Terraform core) and OptionalForImport + // attributes may be null, but could have a config value. + Identity *tfsdk.ResourceIdentity + IdentitySchema fwschema.Schema + Resource resource.Resource // EmptyState is an empty State for the resource schema. This is used to @@ -132,6 +148,28 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta Private: privateProviderData, } + // If the resource supports identity and we are not importing by identity, pre-populate with a null value. + if req.Identity == nil && req.IdentitySchema != nil { + nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullTfValue.Copy(), + } + } + + if req.Identity != nil { + importReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.Identity.Schema, + Raw: req.Identity.Raw.Copy(), + } + + importResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.Identity.Schema, + Raw: req.Identity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource ImportState") resourceWithImportState.ImportState(ctx, importReq, &importResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ImportState") @@ -142,7 +180,21 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } - if importResp.State.Raw.Equal(req.EmptyState.Raw) { + // Set any write-only attributes in the import state to null + modifiedState, err := tftypes.Transform(importResp.State.Raw, NullifyWriteOnlyAttributes(ctx, importResp.State.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Import State", + "There was an unexpected error modifying the Import State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + importResp.State.Raw = modifiedState + + // If we are importing by ID, we should ensure that something in the import stub state has been populated, + // otherwise the resource doesn't actually support import, which is a provider issue. + if req.ID != "" && importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", "An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+ @@ -157,10 +209,21 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private.Provider = importResp.Private } + if importResp.Identity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected ImportState Response", + "An unexpected error was encountered when creating the import response. New identity data was returned by the provider import operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + resp.Deferred = importResp.Deferred resp.ImportedResources = []ImportedResource{ { State: importResp.State, + Identity: importResp.Identity, TypeName: req.TypeName, Private: private, }, diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index bd0275a12..ea9b1ac68 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -18,8 +18,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) func TestServerImportResourceState(t *testing.T) { @@ -33,12 +35,33 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + "other_test_id": tftypes.String, + }, + } + + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "write-only": tftypes.String, + "required": tftypes.String, + }, + } + testEmptyStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, nil), "optional": tftypes.NewValue(tftypes.String, nil), "required": tftypes.NewValue(tftypes.String, nil), }) + testEmptyStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, nil), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }) + testUnknownStateValue := tftypes.NewValue(testType, tftypes.UnknownValue) testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ @@ -47,6 +70,16 @@ func TestServerImportResourceState(t *testing.T) { "required": tftypes.NewValue(tftypes.String, nil), }) + testRequestIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "other_test_id": tftypes.NewValue(tftypes.String, nil), + }) + + testImportedResourceIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "other_test_id": tftypes.NewValue(tftypes.String, "new-value-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -61,21 +94,71 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + "other_test_id": identityschema.StringAttribute{ + OptionalForImport: true, + }, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required": schema.StringAttribute{ + Required: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: testEmptyStateValue, Schema: testSchema, } + testEmptyStateWriteOnly := &tfsdk.State{ + Raw: testEmptyStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testUnknownState := &tfsdk.State{ Raw: testUnknownStateValue, Schema: testSchema, } + testRequestIdentity := &tfsdk.ResourceIdentity{ + Raw: testRequestIdentityValue, + Schema: testIdentitySchema, + } + testState := &tfsdk.State{ Raw: testStateValue, Schema: testSchema, } + testStatePassThroughIdentity := &tfsdk.State{ + Raw: tftypes.NewValue(testType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "optional": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + } + + testImportedResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: testImportedResourceIdentityValue, + Schema: testIdentitySchema, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -168,6 +251,47 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "request-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + OtherTestID types.String `tfsdk:"other_test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("unexpected req.Identity value: %s", identityData.TestID.ValueString()) + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testEmptyState, + Identity: testRequestIdentity, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, "request-resourcetype-importstate-not-implemented": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -289,6 +413,97 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("other_test_id"), types.StringValue("new-value-123"))...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testEmptyState, + Identity: testImportedResourceIdentity, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-identity-supported": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("test_id"), types.StringValue("id-123"))...) + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("other_test_id"), types.StringValue("new-value-123"))...) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testState, + Identity: testImportedResourceIdentity, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: testImportedResourceIdentityValue, + Schema: testIdentitySchema, + } + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected ImportState Response", + "An unexpected error was encountered when creating the import response. New identity data was returned by the provider import operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, "response-importedresources-deferral-automatic": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -416,11 +631,158 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyStateWriteOnly, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("write-only"), "write-only-val")...) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id"), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-passthrough-identity-imported-by-id": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "id-123", + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testStatePassThroughIdentity, + Identity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-passthrough-identity-imported-by-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("other_test_id"), types.StringValue("new-value-123"))...) + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testStatePassThroughIdentity, + Identity: testImportedResourceIdentity, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-passthrough-identity-invalid-state-path": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "id-123", + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("not-valid"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("not-valid"), + "State Write Error", + "An unexpected error was encountered trying to retrieve type information at a given path. "+ + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"not-valid\") still remains in the path: could not find attribute or block "+ + "\"not-valid\" in schema", + ), + }, + }, + }, + "response-importedresources-passthrough-identity-invalid-identity-path": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("not-valid"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("not-valid"), + "Resource Identity Read Error", + "An unexpected error was encountered trying to retrieve type information at a given path. "+ + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"not-valid\") still remains in the path: could not find attribute or block "+ + "\"not-valid\" in schema", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_moveresourcestate.go b/internal/fwserver/server_moveresourcestate.go index 480e4a956..b838687d3 100644 --- a/internal/fwserver/server_moveresourcestate.go +++ b/internal/fwserver/server_moveresourcestate.go @@ -65,14 +65,25 @@ type MoveResourceStateRequest struct { // TargetTypeName is the type name of the target resource as given by // Terraform across the protocol. TargetTypeName string + + // SourceIdentity is the identity of the source resource. + // + // Only the underlying JSON field is populated. + SourceIdentity *tfprotov6.RawState + + // SourceIdentitySchemaVersion is the version of the source resource state. + SourceIdentitySchemaVersion int64 + + IdentitySchema fwschema.Schema } // MoveResourceStateResponse is the framework server response for the // MoveResourceState RPC. type MoveResourceStateResponse struct { - Diagnostics diag.Diagnostics - TargetPrivate *privatestate.Data - TargetState *tfsdk.State + Diagnostics diag.Diagnostics + TargetPrivate *privatestate.Data + TargetState *tfsdk.State + TargetIdentity *tfsdk.ResourceIdentity } // MoveResourceState implements the framework server MoveResourceState RPC. @@ -125,12 +136,15 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe for _, resourceStateMover := range resourceStateMovers { moveStateReq := resource.MoveStateRequest{ - SourcePrivate: sourcePrivate, - SourceProviderAddress: req.SourceProviderAddress, - SourceRawState: req.SourceRawState, - SourceSchemaVersion: req.SourceSchemaVersion, - SourceTypeName: req.SourceTypeName, + SourcePrivate: sourcePrivate, + SourceProviderAddress: req.SourceProviderAddress, + SourceRawState: req.SourceRawState, + SourceSchemaVersion: req.SourceSchemaVersion, + SourceTypeName: req.SourceTypeName, + SourceIdentity: req.SourceIdentity, + SourceIdentitySchemaVersion: req.SourceIdentitySchemaVersion, } + moveStateResp := resource.MoveStateResponse{ TargetPrivate: privatestate.EmptyProviderData(ctx), TargetState: tfsdk.State{ @@ -139,6 +153,13 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe }, } + if req.IdentitySchema != nil { + moveStateResp.TargetIdentity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil), + Schema: req.IdentitySchema, + } + } + if resourceStateMover.SourceSchema != nil { logging.FrameworkTrace(ctx, "Attempting to populate MoveResourceStateRequest SourceState from provider defined SourceSchema") @@ -205,10 +226,35 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe return } + // Set any write-only attributes in the move resource state to null + modifiedState, err := tftypes.Transform(moveStateResp.TargetState.Raw, NullifyWriteOnlyAttributes(ctx, moveStateResp.TargetState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Move Resource State", + "There was an unexpected error modifying the Move Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + moveStateResp.TargetState.Raw = modifiedState + // If the implement has set the state in any way, return the response. if !moveStateResp.TargetState.Raw.Equal(tftypes.NewValue(req.TargetResourceSchema.Type().TerraformType(ctx), nil)) { resp.Diagnostics = moveStateResp.Diagnostics resp.TargetState = &moveStateResp.TargetState + if moveStateResp.TargetIdentity != nil { + resp.TargetIdentity = moveStateResp.TargetIdentity + } + + if resp.TargetIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Move State Response", + "An unexpected error was encountered when creating the move state response. New identity data was returned by the provider move state operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } if moveStateResp.TargetPrivate != nil { resp.TargetPrivate.Provider = moveStateResp.TargetPrivate diff --git a/internal/fwserver/server_moveresourcestate_test.go b/internal/fwserver/server_moveresourcestate_test.go index 1a8e0fb77..90e32aeb4 100644 --- a/internal/fwserver/server_moveresourcestate_test.go +++ b/internal/fwserver/server_moveresourcestate_test.go @@ -6,9 +6,12 @@ package fwserver_test import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -18,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerMoveResourceState(t *testing.T) { @@ -38,8 +40,35 @@ func TestServerMoveResourceState(t *testing.T) { }, }, } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + schemaType := testSchema.Type().TerraformType(ctx) + schemaIdentityType := testIdentitySchema.Type().TerraformType(ctx) + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.MoveResourceStateRequest @@ -757,11 +786,208 @@ func TestServerMoveResourceState(t *testing.T) { }, }, }, + "response-TargetState-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "write_only_attribute": nil, + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("write_only_attribute"), "movestate-val")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchemaWriteOnly, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, + "request-SourceIdentitySchemaVersion": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + // SourceRawState required to prevent error + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expected := int64(123) + + if diff := cmp.Diff(req.SourceIdentitySchemaVersion, expected); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentitySchemaVersion difference", diff) + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + }, + }, + "request-SourceIdentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + // SourceRawState required to prevent error + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_id": "test_id_value", + }), + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expectedSourceIdentity := testNewRawState(t, map[string]interface{}{ + "test_id": "test_id_value", + }) + + if diff := cmp.Diff(req.SourceIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test_id_value")...) + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + IdentitySchema: testIdentitySchema, + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + TargetIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test_id_value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.TargetIdentity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchema.Type().TerraformType(ctx), map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "should-not-be-set"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Move State Response", + "An unexpected error was encountered when creating the move state response. New identity data was returned by the provider move state operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + TargetIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "should-not-be-set"), + }), + Schema: testIdentitySchema, + }, + TargetPrivate: privatestate.EmptyData(ctx), + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_openephemeralresource_test.go b/internal/fwserver/server_openephemeralresource_test.go index ec2649430..9f0365a92 100644 --- a/internal/fwserver/server_openephemeralresource_test.go +++ b/internal/fwserver/server_openephemeralresource_test.go @@ -381,8 +381,6 @@ func TestServerOpenEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index d66fc4897..fb6c161d8 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -30,9 +30,11 @@ type PlanResourceChangeRequest struct { Config *tfsdk.Config PriorPrivate *privatestate.Data PriorState *tfsdk.State + PriorIdentity *tfsdk.ResourceIdentity ProposedNewState *tfsdk.Plan ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema Resource resource.Resource ResourceBehavior resource.ResourceBehavior } @@ -44,6 +46,7 @@ type PlanResourceChangeResponse struct { Diagnostics diag.Diagnostics PlannedPrivate *privatestate.Data PlannedState *tfsdk.State + PlannedIdentity *tfsdk.ResourceIdentity RequiresReplace path.Paths } @@ -115,6 +118,24 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + // If the resource supports identity and there is no prior identity data, pre-populate with a null value. + if req.PriorIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PriorIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Set the planned identity to the prior identity by default (can be modified later). + if req.PriorIdentity != nil { + resp.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.PriorIdentity.Schema, + Raw: req.PriorIdentity.Raw.Copy(), + } + } + // Ensure that resp.PlannedPrivate is never nil. resp.PlannedPrivate = privatestate.EmptyData(ctx) @@ -155,6 +176,18 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.PlannedState.Raw = data.TerraformValue } + // Set any write-only attributes in the plan to null + modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, NullifyWriteOnlyAttributes(ctx, resp.PlannedState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Planned State", + "There was an unexpected error modifying the PlannedState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + resp.PlannedState.Raw = modifiedPlan + // After ensuring there are proposed changes, mark any computed attributes // that are null in the config as unknown in the plan, so providers have // the choice to update them. @@ -292,9 +325,17 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange modifyPlanReq.ProviderMeta = *req.ProviderMeta } + if resp.PlannedIdentity != nil { + modifyPlanReq.Identity = &tfsdk.ResourceIdentity{ + Schema: resp.PlannedIdentity.Schema, + Raw: resp.PlannedIdentity.Raw.Copy(), + } + } + modifyPlanResp := resource.ModifyPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, + Identity: modifyPlanReq.Identity, RequiresReplace: path.Paths{}, Private: modifyPlanReq.Private, } @@ -305,6 +346,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.Diagnostics = modifyPlanResp.Diagnostics resp.PlannedState = planToState(modifyPlanResp.Plan) + resp.PlannedIdentity = modifyPlanResp.Identity resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private resp.Deferred = modifyPlanResp.Deferred @@ -326,6 +368,16 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + if resp.PlannedIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Plan Response", + "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + // Ensure deterministic RequiresReplace by sorting and deduplicating resp.RequiresReplace = NormaliseRequiresReplace(ctx, resp.RequiresReplace) @@ -337,6 +389,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange "This is always an issue in the Terraform Provider and should be reported to the provider developers.\n\n"+ "Ensure all resource plan modifiers do not attempt to change resource plan data from being a null value if the request plan is a null value.", ) + return } } diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 66e693935..e8cbf3ad7 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicdefault" @@ -405,7 +406,6 @@ func TestNormaliseRequiresReplace(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -438,6 +438,20 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchemaTypeDefault := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_computed_bool": tftypes.Bool, @@ -567,6 +581,29 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + testSchemaDefault := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed_bool": schema.BoolAttribute{ @@ -1070,6 +1107,11 @@ func TestServerPlanResourceChange(t *testing.T) { Schema: testSchema, } + testEmptyStateWriteOnly := &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, nil), + Schema: testSchemaWriteOnly, + } + testEmptyStateDefault := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaTypeDefault, nil), Schema: testSchemaDefault, @@ -1080,6 +1122,10 @@ func TestServerPlanResourceChange(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataBlock struct { TestRequired types.String `tfsdk:"test_required"` TestOptionalBlock types.Object `tfsdk:"test_optional_block"` @@ -1368,6 +1414,43 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-mark-computed-config-nils-as-unknown-write-only": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWriteOnly, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: testEmptyStateWriteOnly, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{}, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-set-default-values": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3008,6 +3091,66 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-request-prioridentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+data.TestID.ValueString()) + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3356,6 +3499,124 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-response-plannedidentity-new": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + // Resource supports identity but there isn't one in state yet + PriorIdentity: nil, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if !req.Identity.Raw.IsNull() { + resp.Diagnostics.AddError("Unexpected request", "expected req.Identity to be null") + return + } + + data := testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + } + + resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-plannedidentity-update": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3492,6 +3753,63 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testPrivateProvider, }, }, + "create-resourcewithmodifyplan-response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Plan Response", + "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "delete-resourcewithmodifyplan-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3884,6 +4202,50 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "update-mark-computed-config-nils-as-unknown-write-only": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWriteOnly, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "prior-state-val"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "prior-state-val"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{}, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "prior-state-val"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "update-set-default-values": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -5596,6 +5958,13 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, ResourceSchema: testSchemaAttributePlanModifierAttributePlan, Resource: &testprovider.Resource{}, }, @@ -5614,6 +5983,12 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, PlannedPrivate: testEmptyPrivate, }, }, @@ -6343,8 +6718,6 @@ func TestServerPlanResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -15958,8 +16331,6 @@ func TestServerPlanResourceChange_AttributeRoundtrip(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_readdatasource_test.go b/internal/fwserver/server_readdatasource_test.go index d2f91bd37..45334d43e 100644 --- a/internal/fwserver/server_readdatasource_test.go +++ b/internal/fwserver/server_readdatasource_test.go @@ -453,8 +453,6 @@ func TestServerReadDataSource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 628a2e445..557b1a714 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -6,7 +6,10 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -18,6 +21,8 @@ import ( // ReadResource RPC. type ReadResourceRequest struct { ClientCapabilities resource.ReadClientCapabilities + IdentitySchema fwschema.Schema + CurrentIdentity *tfsdk.ResourceIdentity CurrentState *tfsdk.State Resource resource.Resource Private *privatestate.Data @@ -30,6 +35,7 @@ type ReadResourceResponse struct { Deferred *resource.Deferred Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -113,12 +119,35 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Private = req.Private } + // If the resource supports identity and there is no current identity data, pre-populate with a null value. + if req.CurrentIdentity == nil && req.IdentitySchema != nil { + nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.CurrentIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullTfValue.Copy(), + } + } + + if req.CurrentIdentity != nil { + readReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), + } + + readResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Read") req.Resource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Read") resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State + resp.NewIdentity = readResp.Identity resp.Deferred = readResp.Deferred if readResp.Private != nil { @@ -133,6 +162,16 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Read Response", + "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, @@ -157,11 +196,21 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index a9520edd4..a846d389c 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -34,16 +35,42 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_write_only": tftypes.String, + "test_required": tftypes.String, + }, + } + testCurrentStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testCurrentIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testCurrentStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }) + testNewStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-newstate-value"), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testNewIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -55,6 +82,26 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -97,11 +144,26 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testCurrentIdentity := &tfsdk.ResourceIdentity{ + Raw: testCurrentIdentityValue, + Schema: testIdentitySchema, + } + + testCurrentStateWriteOnly := &tfsdk.State{ + Raw: testCurrentStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testNewState := &tfsdk.State{ Raw: testNewStateValue, Schema: testSchema, } + testNewIdentity := &tfsdk.ResourceIdentity{ + Raw: testNewIdentityValue, + Schema: testIdentitySchema, + } + testNewStateRemoved := &tfsdk.State{ Raw: tftypes.NewValue(testType, nil), Schema: testSchema, @@ -220,6 +282,36 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-currentidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + IdentitySchema: testIdentitySchema, + CurrentIdentity: testCurrentIdentity, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("unexpected req.Identity value: %s", identityData.TestID.ValueString()) + } + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testCurrentIdentity, + Private: testEmptyPrivate, + }, + }, "request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -503,6 +595,99 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-identity-new": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + // Resource supports identity but there isn't one in state yet + CurrentIdentity: nil, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if !req.Identity.Raw.IsNull() { + resp.Diagnostics.AddError("Unexpected request", "expected req.Identity to be null") + return + } + + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-identity-update": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + CurrentIdentity: testCurrentIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: testNewIdentityValue, + Schema: testIdentitySchema, + } + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Read Response", + "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, "response-state-removeresource": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -562,6 +747,38 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-state-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentStateWriteOnly, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestWriteOnly types.String `tfsdk:"test_write_only"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + data.TestWriteOnly = types.StringValue("test-write-only-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -604,8 +821,6 @@ func TestServerReadResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_renewephemeralresource_test.go b/internal/fwserver/server_renewephemeralresource_test.go index 368a5aa87..8cc6e01a6 100644 --- a/internal/fwserver/server_renewephemeralresource_test.go +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -262,8 +262,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 9112c35c2..041dee187 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -20,13 +20,15 @@ import ( // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // UpdateResourceResponse is the framework server response for an update request @@ -34,6 +36,7 @@ type UpdateResourceRequest struct { type UpdateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -118,12 +121,36 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, resp.Private = req.PlannedPrivate } + // If the resource supports identity and there is no planned identity data, pre-populate with a null value. + if req.PlannedIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Pre-populate the new identity with the planned identity. + if req.PlannedIdentity != nil { + updateReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + + updateResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Update") req.Resource.Update(ctx, updateReq, &updateResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Update") resp.Diagnostics = updateResp.Diagnostics resp.NewState = &updateResp.State + resp.NewIdentity = updateResp.Identity if !resp.Diagnostics.HasError() && updateResp.State.Raw.Equal(nullSchemaData) { resp.Diagnostics.AddError( @@ -145,6 +172,16 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Update Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, @@ -169,11 +206,21 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 6ad7a38d6..f17616703 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -34,6 +35,19 @@ func TestServerUpdateResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -45,6 +59,14 @@ func TestServerUpdateResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -77,16 +99,37 @@ func TestServerUpdateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataWithSemanticEquals struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -250,6 +293,63 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "request-priorstate": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -675,6 +775,110 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Update Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -777,6 +981,39 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -859,8 +1096,6 @@ func TestServerUpdateResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_upgraderesourcestate.go b/internal/fwserver/server_upgraderesourcestate.go index b2cc340e5..e64a1339c 100644 --- a/internal/fwserver/server_upgraderesourcestate.go +++ b/internal/fwserver/server_upgraderesourcestate.go @@ -225,9 +225,19 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradedStateValue, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + resp.UpgradedState = &tfsdk.State{ Schema: req.ResourceSchema, - Raw: upgradedStateValue, + Raw: modifiedState, } return @@ -243,5 +253,16 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradeResourceStateResponse.State.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + upgradeResourceStateResponse.State.Raw = modifiedState + resp.UpgradedState = &upgradeResourceStateResponse.State } diff --git a/internal/fwserver/server_upgraderesourcestate_test.go b/internal/fwserver/server_upgraderesourcestate_test.go index b720bed7e..00ee46092 100644 --- a/internal/fwserver/server_upgraderesourcestate_test.go +++ b/internal/fwserver/server_upgraderesourcestate_test.go @@ -42,6 +42,23 @@ func TestServerUpgradeResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + Version: 1, // Must be above 0 + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.UpgradeResourceStateRequest @@ -342,6 +359,71 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "RawState-DynamicValue-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var rawState struct { + Id string `json:"id"` + RequiredAttribute bool `json:"required_attribute"` + } + + if err := json.Unmarshal(req.RawState.JSON, &rawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior State", + err.Error(), + ) + return + } + + dynamicValue, err := tfprotov6.NewDynamicValue( + schemaTypeWriteOnly, + tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, rawState.Id), + "write_only_attribute": tftypes.NewValue(tftypes.String, "write-only-dynamic-value"), + "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", rawState.RequiredAttribute)), + }), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Upgraded State", + err.Error(), + ) + return + } + + resp.DynamicValue = &dynamicValue + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "ResourceType-UpgradeState-not-implemented": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -517,6 +599,72 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "PriorSchema-and-State-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "required_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData struct { + Id string `tfsdk:"id"` + RequiredAttribute bool `tfsdk:"required_attribute"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := struct { + Id string `tfsdk:"id"` + WriteOnlyAttribute string `tfsdk:"write_only_attribute"` + RequiredAttribute string `tfsdk:"required_attribute"` + }{ + Id: priorStateData.Id, + WriteOnlyAttribute: "write-only-upgraded-state", + RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...) + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "PriorSchema-and-State-json-mismatch": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -765,8 +913,6 @@ func TestServerUpgradeResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_validatedatasourceconfig.go b/internal/fwserver/server_validatedatasourceconfig.go index 3379b15ad..33653982d 100644 --- a/internal/fwserver/server_validatedatasourceconfig.go +++ b/internal/fwserver/server_validatedatasourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateDataSourceConfig(ctx context.Context, req *ValidateData resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validatedatasourceconfig_test.go b/internal/fwserver/server_validatedatasourceconfig_test.go index 6ff93062a..3b669dfcb 100644 --- a/internal/fwserver/server_validatedatasourceconfig_test.go +++ b/internal/fwserver/server_validatedatasourceconfig_test.go @@ -292,8 +292,6 @@ func TestServerValidateDataSourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_validateephemeralresourceconfig.go b/internal/fwserver/server_validateephemeralresourceconfig.go index a99a0dbfb..6956af068 100644 --- a/internal/fwserver/server_validateephemeralresourceconfig.go +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, req *Valid resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateephemeralresourceconfig_test.go b/internal/fwserver/server_validateephemeralresourceconfig_test.go index 24cb08e70..0497cd7d1 100644 --- a/internal/fwserver/server_validateephemeralresourceconfig_test.go +++ b/internal/fwserver/server_validateephemeralresourceconfig_test.go @@ -292,8 +292,6 @@ func TestServerValidateEphemeralResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_validateproviderconfig.go b/internal/fwserver/server_validateproviderconfig.go index 588f021c2..0109e6e07 100644 --- a/internal/fwserver/server_validateproviderconfig.go +++ b/internal/fwserver/server_validateproviderconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -77,8 +78,17 @@ func (s *Server) ValidateProviderConfig(ctx context.Context, req *ValidateProvid resp.Diagnostics.Append(vpcRes.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateproviderconfig_test.go b/internal/fwserver/server_validateproviderconfig_test.go index 49574e60c..b2d71483d 100644 --- a/internal/fwserver/server_validateproviderconfig_test.go +++ b/internal/fwserver/server_validateproviderconfig_test.go @@ -298,8 +298,6 @@ func TestServerValidateProviderConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_validateresourceconfig.go b/internal/fwserver/server_validateresourceconfig.go index 79e8ae9b7..591ce5a2f 100644 --- a/internal/fwserver/server_validateresourceconfig.go +++ b/internal/fwserver/server_validateresourceconfig.go @@ -9,14 +9,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) // ValidateResourceConfigRequest is the framework server request for the // ValidateResourceConfig RPC. type ValidateResourceConfigRequest struct { - Config *tfsdk.Config - Resource resource.Resource + ClientCapabilities resource.ValidateConfigClientCapabilities + Config *tfsdk.Config + Resource resource.Resource } // ValidateResourceConfigResponse is the framework server response for the @@ -51,7 +53,8 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour } vdscReq := resource.ValidateConfigRequest{ - Config: *req.Config, + ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, } if resourceWithConfigValidators, ok := req.Resource.(resource.ResourceWithConfigValidators); ok { @@ -96,8 +99,13 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: req.ClientCapabilities.WriteOnlyAttributesAllowed, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateresourceconfig_test.go b/internal/fwserver/server_validateresourceconfig_test.go index 859612031..59369e9df 100644 --- a/internal/fwserver/server_validateresourceconfig_test.go +++ b/internal/fwserver/server_validateresourceconfig_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateResourceConfig(t *testing.T) { @@ -47,6 +48,10 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchema, } + testClientCapabilities := resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + } + testSchemaAttributeValidator := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -69,6 +74,28 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchemaAttributeValidator, } + testSchemaAttributeValidatorClientCapabilities := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorClientCapabilities := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorClientCapabilities, + } + testSchemaAttributeValidatorError := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -128,6 +155,21 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-AttributeValidator-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfigAttributeValidatorClientCapabilities, + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorClientCapabilities + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-AttributeValidator-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -185,6 +227,34 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithConfigValidators-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithConfigValidators{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + &testprovider.ResourceConfigValidator{ + ValidateResourceMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithConfigValidators-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -259,6 +329,28 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithValidateConfig-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithValidateConfig{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithValidateConfig-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -292,8 +384,6 @@ func TestServerValidateResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/write_only_nullification.go b/internal/fwserver/write_only_nullification.go new file mode 100644 index 000000000..0d95152e8 --- /dev/null +++ b/internal/fwserver/write_only_nullification.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" +) + +// NullifyWriteOnlyAttributes transforms a tftypes.Value, setting all write-only attribute values +// to null according to the given managed resource schema. This function is called in all managed +// resource RPCs before a response is sent to Terraform Core. Terraform Core expects all write-only +// attribute values to be null to prevent data consistency errors. This can technically be done +// manually by the provider developers, but the Framework is handling it instead for convenience. +func NullifyWriteOnlyAttributes(ctx context.Context, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { + return func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) { + ctx = logging.FrameworkWithAttributePath(ctx, path.String()) + + // we are only modifying attributes, not the entire resource + if len(path.Steps()) < 1 { + return val, nil + } + + attribute, err := resourceSchema.AttributeAtTerraformPath(ctx, path) + + if err != nil { + if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { + // ignore attributes/elements inside schema.Attributes, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is a non-schema attribute, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathIsBlock) { + // ignore blocks, they do not have a writeOnly field + logging.FrameworkTrace(ctx, "attribute is a block, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { + // ignore attributes/elements inside schema.DynamicAttribute, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is inside of a dynamic attribute, not nullifying") + return val, nil + } + + logging.FrameworkError(ctx, "couldn't find attribute in resource schema") + + return tftypes.Value{}, fmt.Errorf("couldn't find attribute in resource schema: %w", err) + } + + // Value type from new state to create null with + newValueType := attribute.GetType().TerraformType(ctx) + + if attribute.IsWriteOnly() && !val.IsNull() { + logging.FrameworkDebug(ctx, "Nullifying write-only attribute in the newState") + + return tftypes.NewValue(newValueType, nil), nil + } + + return val, nil + } +} diff --git a/internal/fwserver/write_only_nullification_test.go b/internal/fwserver/write_only_nullification_test.go new file mode 100644 index 000000000..47ca9780f --- /dev/null +++ b/internal/fwserver/write_only_nullification_test.go @@ -0,0 +1,1259 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNullifyWriteOnlyAttributes(t *testing.T) { + t.Parallel() + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string-value": schema.StringAttribute{ + Required: true, + }, + "string-nil": schema.StringAttribute{ + Optional: true, + }, + "string-nil-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-value-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value": schema.DynamicAttribute{ + Required: true, + }, + "dynamic-nil": schema.DynamicAttribute{ + Optional: true, + }, + "dynamic-nil-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-with-underlying-list-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "object-nil-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "object-value-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "nested-nil-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "nested-value-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "block-nil-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "block-value-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Bool, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, false), + }, + ), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "foo"), + }), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, nil), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, nil), + }), + }), + }) + + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} + +func TestNullifyWriteOnlyAttributes_NestedTypes(t *testing.T) { + t.Parallel() + nestedObjectType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + }, + } + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + "list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + "list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, nil), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, nil), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, nil), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, nil), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + }), + }), + }) + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} diff --git a/internal/fwtype/missing_underlying_type_validation_test.go b/internal/fwtype/missing_underlying_type_validation_test.go index dea491b11..e8b5d3adb 100644 --- a/internal/fwtype/missing_underlying_type_validation_test.go +++ b/internal/fwtype/missing_underlying_type_validation_test.go @@ -2812,8 +2812,6 @@ func TestContainsMissingUnderlyingType(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/fwtype/static_collection_validation.go b/internal/fwtype/static_collection_validation.go index 2a91e2fbb..50a4b9f2d 100644 --- a/internal/fwtype/static_collection_validation.go +++ b/internal/fwtype/static_collection_validation.go @@ -5,7 +5,6 @@ package fwtype import ( "fmt" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -140,3 +139,19 @@ func ReturnCollectionWithDynamicTypeDiag() diag.Diagnostic { "If underlying dynamic values are required, replace the return definition with DynamicReturn instead.", ) } + +// IsAllowedPrimitiveType checks if the given attr.Type is an allowed primitive type for resource identity. +func IsAllowedPrimitiveType(typ attr.Type) bool { + switch typ.(type) { + case basetypes.BoolTypable, + basetypes.Float64Typable, + basetypes.Float32Typable, + basetypes.Int64Typable, + basetypes.Int32Typable, + basetypes.NumberTypable, + basetypes.StringTypable: + return true + default: + return false + } +} diff --git a/internal/fwtype/static_collection_validation_test.go b/internal/fwtype/static_collection_validation_test.go index e7ea5e722..058ee3aa9 100644 --- a/internal/fwtype/static_collection_validation_test.go +++ b/internal/fwtype/static_collection_validation_test.go @@ -933,7 +933,6 @@ func TestTypeContainsCollectionWithDynamic(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase t.Run(name, func(t *testing.T) { t.Parallel() @@ -945,3 +944,82 @@ func TestTypeContainsCollectionWithDynamic(t *testing.T) { }) } } + +func TestIsAllowedPrimitiveType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attrTyp attr.Type + expected bool + }{ + "nil": { + attrTyp: nil, + expected: false, + }, + "dynamic": { + attrTyp: types.DynamicType, + expected: false, + }, + "bool": { + attrTyp: types.BoolType, + expected: true, + }, + "int64": { + attrTyp: types.Int64Type, + expected: true, + }, + "int32": { + attrTyp: types.Int32Type, + expected: true, + }, + "float64": { + attrTyp: types.Float64Type, + expected: true, + }, + "float32": { + attrTyp: types.Float32Type, + expected: true, + }, + "number": { + attrTyp: types.NumberType, + expected: true, + }, + "string": { + attrTyp: types.StringType, + expected: true, + }, + "list-missing": { + attrTyp: types.ListType{}, + expected: false, + }, + "list-static": { + attrTyp: types.ListType{ + ElemType: types.StringType, + }, + expected: false, + }, + "map": { + attrTyp: types.MapType{}, + expected: false, + }, + "object": { + attrTyp: types.ObjectType{}, + expected: false, + }, + "tuple": { + attrTyp: types.TupleType{}, + expected: false, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fwtype.IsAllowedPrimitiveType(testCase.attrTyp) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/privatestate/data_test.go b/internal/privatestate/data_test.go index ae40c2f8f..d1bce0f32 100644 --- a/internal/privatestate/data_test.go +++ b/internal/privatestate/data_test.go @@ -200,8 +200,6 @@ func TestData_Bytes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -431,8 +429,6 @@ func TestNewData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -538,8 +534,6 @@ func TestNewProviderData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -631,8 +625,6 @@ func TestProviderDataEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -693,8 +685,6 @@ func TestProviderData_GetKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -938,8 +928,6 @@ func TestProviderData_SetKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -977,8 +965,6 @@ func TestValidateProviderDataKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_applyresourcechange.go b/internal/proto5server/server_applyresourcechange.go index e4e8bb92e..717d071f2 100644 --- a/internal/proto5server/server_applyresourcechange.go +++ b/internal/proto5server/server_applyresourcechange.go @@ -36,6 +36,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto5Req *tfprotov5.A return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto5Req *tfprotov5.A return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index 92e0a1b24..6c17d703b 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -34,6 +35,20 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testPlannedIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -45,6 +60,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -194,6 +217,79 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -372,6 +468,73 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-response-newidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + }, + }, "create-response-newstate-null": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1416,8 +1579,6 @@ func TestServerApplyResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_callfunction_test.go b/internal/proto5server/server_callfunction_test.go index 17f119267..632e18f5a 100644 --- a/internal/proto5server/server_callfunction_test.go +++ b/internal/proto5server/server_callfunction_test.go @@ -235,8 +235,6 @@ func TestServerCallFunction(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_closeephemeralresource_test.go b/internal/proto5server/server_closeephemeralresource_test.go index 46d987d1d..6f575786f 100644 --- a/internal/proto5server/server_closeephemeralresource_test.go +++ b/internal/proto5server/server_closeephemeralresource_test.go @@ -112,8 +112,6 @@ func TestServerCloseEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_configureprovider_test.go b/internal/proto5server/server_configureprovider_test.go index f538af6b9..06d4df619 100644 --- a/internal/proto5server/server_configureprovider_test.go +++ b/internal/proto5server/server_configureprovider_test.go @@ -153,8 +153,6 @@ func TestServerConfigureProvider(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_getfunctions_test.go b/internal/proto5server/server_getfunctions_test.go index 07060e83f..ad8e4a04e 100644 --- a/internal/proto5server/server_getfunctions_test.go +++ b/internal/proto5server/server_getfunctions_test.go @@ -160,8 +160,6 @@ func TestServerGetFunctions(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index 39d2985bd..9f8baf1fc 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -554,8 +554,6 @@ func TestServerGetMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 41a632391..590d87a62 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -865,8 +865,6 @@ func TestServerGetProviderSchema(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_getresourceidentityschemas.go b/internal/proto5server/server_getresourceidentityschemas.go new file mode 100644 index 000000000..97ecb47ea --- /dev/null +++ b/internal/proto5server/server_getresourceidentityschemas.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemas satisfies the tfprotov5.ProviderServer interface. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, proto5Req *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto5.GetResourceIdentitySchemasRequest(ctx, proto5Req) + fwResp := &fwserver.GetResourceIdentitySchemasResponse{} + + s.FrameworkServer.GetResourceIdentitySchemas(ctx, fwReq, fwResp) + + return toproto5.GetResourceIdentitySchemasResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_getresourceidentityschemas_test.go b/internal/proto5server/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..e054e2e9b --- /dev/null +++ b/internal/proto5server/server_getresourceidentityschemas_test.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov5.GetResourceIdentitySchemasRequest + expectedError error + expectedResponse *tfprotov5.GetResourceIdentitySchemasResponse + }{ + "resource-identity-schemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test1": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetResourceIdentitySchemasRequest{}, + expectedResponse: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource1": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test1", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + "test_resource2": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test2", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetResourceIdentitySchemas(context.Background(), new(tfprotov5.GetResourceIdentitySchemasRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} + +func TestServerGetResourceIdentitySchemas_logging(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.InitContext(ctx) + + testServer := &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "examplecloud_thing" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{} + }, + } + }, + } + }, + }, + }, + } + + _, err := testServer.GetResourceIdentitySchemas(ctx, new(tfprotov5.GetResourceIdentitySchemasRequest)) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + 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": "Checking ResourceTypes lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Found resource type", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Calling provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Called provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + } + + if diff := cmp.Diff(entries, expectedEntries); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/internal/proto5server/server_importresourcestate.go b/internal/proto5server/server_importresourcestate.go index 5d89dc908..9baaa8056 100644 --- a/internal/proto5server/server_importresourcestate.go +++ b/internal/proto5server/server_importresourcestate.go @@ -36,7 +36,15 @@ func (s *Server) ImportResourceState(ctx context.Context, proto5Req *tfprotov5.I return toproto5.ImportResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ImportResourceStateRequest(ctx, proto5Req, resource, resourceSchema) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ImportResourceStateResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ImportResourceStateRequest(ctx, proto5Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_importresourcestate_test.go b/internal/proto5server/server_importresourcestate_test.go index 8b6b3578e..e2b539199 100644 --- a/internal/proto5server/server_importresourcestate_test.go +++ b/internal/proto5server/server_importresourcestate_test.go @@ -16,7 +16,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" ) func TestServerImportResourceState(t *testing.T) { @@ -30,12 +32,34 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + "test_other_id": tftypes.String, + }, + } + + testRequestIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "test_other_id": tftypes.NewValue(tftypes.String, nil), + }) + + testImportedResourceIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "test_other_id": tftypes.NewValue(tftypes.String, "new-value-123"), + }) + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-id"), "optional": tftypes.NewValue(tftypes.String, nil), "required": tftypes.NewValue(tftypes.String, nil), }) + testEmptyStateDynamicValue, err := tfprotov5.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -50,6 +74,17 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + "test_other_id": identityschema.StringAttribute{ + OptionalForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov5.ImportResourceStateRequest @@ -99,6 +134,64 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "request-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + TestOtherID types.String `tfsdk:"test_other_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test_resource", + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + }, + expectedResponse: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: &testEmptyStateDynamicValue, + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + TypeName: "test_resource", + }, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -184,6 +277,55 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("test_other_id"), types.StringValue("new-value-123"))...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test_resource", + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + }, + expectedResponse: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: &testEmptyStateDynamicValue, + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: testImportedResourceIdentityDynamicValue, + }, + TypeName: "test_resource", + }, + }, + }, + }, "response-importedresources-private": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -233,8 +375,6 @@ func TestServerImportResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_moveresourcestate.go b/internal/proto5server/server_moveresourcestate.go index efa1b118c..23b570ed9 100644 --- a/internal/proto5server/server_moveresourcestate.go +++ b/internal/proto5server/server_moveresourcestate.go @@ -36,11 +36,19 @@ func (s *Server) MoveResourceState(ctx context.Context, proto5Req *tfprotov5.Mov fwResp.Diagnostics.Append(diags...) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TargetTypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.MoveResourceStateResponse(ctx, fwResp), nil + } + if fwResp.Diagnostics.HasError() { return toproto5.MoveResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.MoveResourceStateRequest(ctx, proto5Req, resource, resourceSchema) + fwReq, diags := fromproto5.MoveResourceStateRequest(ctx, proto5Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_moveresourcestate_test.go b/internal/proto5server/server_moveresourcestate_test.go index b558b573c..17b11fc47 100644 --- a/internal/proto5server/server_moveresourcestate_test.go +++ b/internal/proto5server/server_moveresourcestate_test.go @@ -6,6 +6,7 @@ package proto5server import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" @@ -39,6 +40,20 @@ func TestServerMoveResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testCases := map[string]struct { server *Server request *tfprotov5.MoveResourceStateRequest @@ -413,6 +428,140 @@ func TestServerMoveResourceState(t *testing.T) { }), }, }, + "request-SourceIdentitySchemaVersion": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expected := int64(123) + + if diff := cmp.Diff(req.SourceIdentitySchemaVersion, expected); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentitySchemaVersion difference", diff) + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + // SourceState required to prevent error + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, + "request-SourceIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expectedSourceRawState := testNewTfprotov6RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }) + + if diff := cmp.Diff(req.SourceRawState, expectedSourceRawState); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceRawState difference", diff) + } + + expectedSourceIdentity := testNewTfprotov6RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }) + + if diff := cmp.Diff(req.SourceIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test-id-value")...) + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetIdentity: &tfprotov5.ResourceIdentityData{IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + })}, + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, "request-TargetTypeName-missing": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -705,11 +854,67 @@ func TestServerMoveResourceState(t *testing.T) { }), }, }, + "response-TargetIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + TargetIdentity: &tfprotov5.ResourceIdentityData{IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + })}, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_openephemeralresource_test.go b/internal/proto5server/server_openephemeralresource_test.go index a4e7d2113..85e8e911e 100644 --- a/internal/proto5server/server_openephemeralresource_test.go +++ b/internal/proto5server/server_openephemeralresource_test.go @@ -249,8 +249,6 @@ func TestServerOpenEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_planresourcechange.go b/internal/proto5server/server_planresourcechange.go index a5cec1987..68918c532 100644 --- a/internal/proto5server/server_planresourcechange.go +++ b/internal/proto5server/server_planresourcechange.go @@ -37,6 +37,14 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto5Req *tfprotov5.Pl return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -53,7 +61,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto5Req *tfprotov5.Pl return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) + fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index ad16683fb..dbc9e609d 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -33,6 +34,12 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +51,18 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -179,6 +198,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", data.TestID.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -345,6 +428,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-response-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + }, + }, + }, "create-response-requiresreplace": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1039,8 +1186,6 @@ func TestServerPlanResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_prepareproviderconfig_test.go b/internal/proto5server/server_prepareproviderconfig_test.go index 65ae74a28..945171a80 100644 --- a/internal/proto5server/server_prepareproviderconfig_test.go +++ b/internal/proto5server/server_prepareproviderconfig_test.go @@ -124,8 +124,6 @@ func TestServerPrepareProviderConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_readdatasource_test.go b/internal/proto5server/server_readdatasource_test.go index 538786bad..49bbe21af 100644 --- a/internal/proto5server/server_readdatasource_test.go +++ b/internal/proto5server/server_readdatasource_test.go @@ -286,8 +286,6 @@ func TestServerReadDataSource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_readresource.go b/internal/proto5server/server_readresource.go index e9863e148..299b67835 100644 --- a/internal/proto5server/server_readresource.go +++ b/internal/proto5server/server_readresource.go @@ -37,6 +37,14 @@ func (s *Server) ReadResource(ctx context.Context, proto5Req *tfprotov5.ReadReso return toproto5.ReadResourceResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ReadResourceResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -45,7 +53,7 @@ func (s *Server) ReadResource(ctx context.Context, proto5Req *tfprotov5.ReadReso return toproto5.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index 12a2604f3..9617d7b7a 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -46,6 +47,20 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov5.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testCurrentIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testProviderMetaDynamicValue := testNewDynamicValue(t, tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -70,6 +85,14 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov5.ReadResourceRequest @@ -246,6 +269,69 @@ func TestServerReadResource(t *testing.T) { }), }, }, + "request-currentidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, + }, + "test_required": metaschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -332,6 +418,55 @@ func TestServerReadResource(t *testing.T) { NewState: testNewStateDynamicValue, }, }, + "response-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + }, + }, "response-state-removeresource": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -402,8 +537,6 @@ func TestServerReadResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_renewephemeralresource_test.go b/internal/proto5server/server_renewephemeralresource_test.go index 73a63f43b..651a7fc61 100644 --- a/internal/proto5server/server_renewephemeralresource_test.go +++ b/internal/proto5server/server_renewephemeralresource_test.go @@ -146,8 +146,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_upgraderesourceidentity.go b/internal/proto5server/server_upgraderesourceidentity.go new file mode 100644 index 000000000..f5b1979f1 --- /dev/null +++ b/internal/proto5server/server_upgraderesourceidentity.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// UpgradeResourceIdentity satisfies the tfprotov5.ProviderServer interface. +func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto5Req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { + panic("unimplemented") // TODO:ResourceIdentity: implement +} diff --git a/internal/proto5server/server_upgraderesourcestate_test.go b/internal/proto5server/server_upgraderesourcestate_test.go index 727091014..6d391f831 100644 --- a/internal/proto5server/server_upgraderesourcestate_test.go +++ b/internal/proto5server/server_upgraderesourcestate_test.go @@ -267,8 +267,6 @@ func TestServerUpgradeResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_validatedatasourceconfig_test.go b/internal/proto5server/server_validatedatasourceconfig_test.go index 2166ec7ca..0eb8f5ad7 100644 --- a/internal/proto5server/server_validatedatasourceconfig_test.go +++ b/internal/proto5server/server_validatedatasourceconfig_test.go @@ -151,8 +151,6 @@ func TestServerValidateDataSourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_validateephemeralresourceconfig_test.go b/internal/proto5server/server_validateephemeralresourceconfig_test.go index 6505bbf53..7912087e4 100644 --- a/internal/proto5server/server_validateephemeralresourceconfig_test.go +++ b/internal/proto5server/server_validateephemeralresourceconfig_test.go @@ -149,8 +149,6 @@ func TestServerValidateEphemeralResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto5server/server_validateresourcetypeconfig_test.go b/internal/proto5server/server_validateresourcetypeconfig_test.go index 2799a82c1..0a647cfc2 100644 --- a/internal/proto5server/server_validateresourcetypeconfig_test.go +++ b/internal/proto5server/server_validateresourcetypeconfig_test.go @@ -149,8 +149,6 @@ func TestServerValidateResourceTypeConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_applyresourcechange.go b/internal/proto6server/server_applyresourcechange.go index 0762368b1..85fc2dc11 100644 --- a/internal/proto6server/server_applyresourcechange.go +++ b/internal/proto6server/server_applyresourcechange.go @@ -36,6 +36,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto6Req *tfprotov6.A return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto6Req *tfprotov6.A return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index 463371cb7..93b4f40e0 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -34,6 +35,20 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testPlannedIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -45,6 +60,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -194,6 +217,79 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -372,6 +468,73 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-response-newidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + }, + }, "create-response-newstate-null": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1416,8 +1579,6 @@ func TestServerApplyResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_callfunction_test.go b/internal/proto6server/server_callfunction_test.go index e4710ff26..15cfe6170 100644 --- a/internal/proto6server/server_callfunction_test.go +++ b/internal/proto6server/server_callfunction_test.go @@ -234,8 +234,6 @@ func TestServerCallFunction(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_closeephemeralresource_test.go b/internal/proto6server/server_closeephemeralresource_test.go index f1c732bcc..795343de1 100644 --- a/internal/proto6server/server_closeephemeralresource_test.go +++ b/internal/proto6server/server_closeephemeralresource_test.go @@ -112,8 +112,6 @@ func TestServerCloseEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_configureprovider_test.go b/internal/proto6server/server_configureprovider_test.go index 13ae83371..a3369846f 100644 --- a/internal/proto6server/server_configureprovider_test.go +++ b/internal/proto6server/server_configureprovider_test.go @@ -153,8 +153,6 @@ func TestServerConfigureProvider(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_getfunctions_test.go b/internal/proto6server/server_getfunctions_test.go index f503f7ef3..b15fc925b 100644 --- a/internal/proto6server/server_getfunctions_test.go +++ b/internal/proto6server/server_getfunctions_test.go @@ -160,8 +160,6 @@ func TestServerGetFunctions(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 5a2395f7e..b32933a65 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -554,8 +554,6 @@ func TestServerGetMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index 24337c998..0b60159de 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -865,8 +865,6 @@ func TestServerGetProviderSchema(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_getresourceidentityschemas.go b/internal/proto6server/server_getresourceidentityschemas.go new file mode 100644 index 000000000..5258d024c --- /dev/null +++ b/internal/proto6server/server_getresourceidentityschemas.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemas satisfies the tfprotov6.ProviderServer interface. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, proto6Req *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto6.GetResourceIdentitySchemasRequest(ctx, proto6Req) + fwResp := &fwserver.GetResourceIdentitySchemasResponse{} + + s.FrameworkServer.GetResourceIdentitySchemas(ctx, fwReq, fwResp) + + return toproto6.GetResourceIdentitySchemasResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_getresourceidentityschemas_test.go b/internal/proto6server/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..d5d7ddfca --- /dev/null +++ b/internal/proto6server/server_getresourceidentityschemas_test.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov6.GetResourceIdentitySchemasRequest + expectedError error + expectedResponse *tfprotov6.GetResourceIdentitySchemasResponse + }{ + "resource-identity-schemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test1": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetResourceIdentitySchemasRequest{}, + expectedResponse: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource1": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test1", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + "test_resource2": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test2", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetResourceIdentitySchemas(context.Background(), new(tfprotov6.GetResourceIdentitySchemasRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} + +func TestServerGetResourceIdentitySchemas_logging(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.InitContext(ctx) + + testServer := &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "examplecloud_thing" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{} + }, + } + }, + } + }, + }, + }, + } + + _, err := testServer.GetResourceIdentitySchemas(ctx, new(tfprotov6.GetResourceIdentitySchemasRequest)) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + 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": "Checking ResourceTypes lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Found resource type", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Calling provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Called provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + } + + if diff := cmp.Diff(entries, expectedEntries); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/internal/proto6server/server_importresourcestate.go b/internal/proto6server/server_importresourcestate.go index 46a58c633..aaf7cf1b8 100644 --- a/internal/proto6server/server_importresourcestate.go +++ b/internal/proto6server/server_importresourcestate.go @@ -36,7 +36,15 @@ func (s *Server) ImportResourceState(ctx context.Context, proto6Req *tfprotov6.I return toproto6.ImportResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ImportResourceStateRequest(ctx, proto6Req, resource, resourceSchema) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ImportResourceStateResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ImportResourceStateRequest(ctx, proto6Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_importresourcestate_test.go b/internal/proto6server/server_importresourcestate_test.go index 024756df2..f59f287b4 100644 --- a/internal/proto6server/server_importresourcestate_test.go +++ b/internal/proto6server/server_importresourcestate_test.go @@ -16,7 +16,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" ) func TestServerImportResourceState(t *testing.T) { @@ -30,12 +32,34 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + "test_other_id": tftypes.String, + }, + } + + testRequestIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "test_other_id": tftypes.NewValue(tftypes.String, nil), + }) + + testImportedResourceIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + "test_other_id": tftypes.NewValue(tftypes.String, "new-value-123"), + }) + testStateDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-id"), "optional": tftypes.NewValue(tftypes.String, nil), "required": tftypes.NewValue(tftypes.String, nil), }) + testEmptyStateDynamicValue, err := tfprotov6.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -50,6 +74,17 @@ func TestServerImportResourceState(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + "test_other_id": identityschema.StringAttribute{ + OptionalForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov6.ImportResourceStateRequest @@ -99,6 +134,64 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "request-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + TestOtherID types.String `tfsdk:"test_other_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ImportResourceStateRequest{ + TypeName: "test_resource", + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + }, + expectedResponse: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: &testEmptyStateDynamicValue, + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + TypeName: "test_resource", + }, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -184,6 +277,55 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndImportState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("test_other_id"), types.StringValue("new-value-123"))...) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ImportResourceStateRequest{ + TypeName: "test_resource", + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: testRequestIdentityValue, + }, + }, + expectedResponse: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: &testEmptyStateDynamicValue, + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: testImportedResourceIdentityDynamicValue, + }, + TypeName: "test_resource", + }, + }, + }, + }, "response-importedresources-private": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -233,8 +375,6 @@ func TestServerImportResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_moveresourcestate.go b/internal/proto6server/server_moveresourcestate.go index 1010b5d76..96e3a96cb 100644 --- a/internal/proto6server/server_moveresourcestate.go +++ b/internal/proto6server/server_moveresourcestate.go @@ -5,7 +5,6 @@ package proto6server import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -36,11 +35,19 @@ func (s *Server) MoveResourceState(ctx context.Context, proto6Req *tfprotov6.Mov fwResp.Diagnostics.Append(diags...) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TargetTypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.MoveResourceStateResponse(ctx, fwResp), nil + } + if fwResp.Diagnostics.HasError() { return toproto6.MoveResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.MoveResourceStateRequest(ctx, proto6Req, resource, resourceSchema) + fwReq, diags := fromproto6.MoveResourceStateRequest(ctx, proto6Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_moveresourcestate_test.go b/internal/proto6server/server_moveresourcestate_test.go index 832bcc89d..22cf82154 100644 --- a/internal/proto6server/server_moveresourcestate_test.go +++ b/internal/proto6server/server_moveresourcestate_test.go @@ -708,8 +708,6 @@ func TestServerMoveResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_openephemeralresource_test.go b/internal/proto6server/server_openephemeralresource_test.go index 18167e499..718dd4d3b 100644 --- a/internal/proto6server/server_openephemeralresource_test.go +++ b/internal/proto6server/server_openephemeralresource_test.go @@ -249,8 +249,6 @@ func TestServerOpenEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_planresourcechange.go b/internal/proto6server/server_planresourcechange.go index 32d13ddd6..cdd057e2f 100644 --- a/internal/proto6server/server_planresourcechange.go +++ b/internal/proto6server/server_planresourcechange.go @@ -37,6 +37,14 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto6Req *tfprotov6.Pl return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -53,7 +61,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto6Req *tfprotov6.Pl return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) + fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 77bd54226..6cd50604f 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -32,6 +33,12 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -43,6 +50,18 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -178,6 +197,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", data.TestID.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -344,6 +427,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-response-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + }, + }, + }, "create-response-requiresreplace": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -1038,8 +1185,6 @@ func TestServerPlanResourceChange(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_readdatasource_test.go b/internal/proto6server/server_readdatasource_test.go index 98a9d20cc..9808a58eb 100644 --- a/internal/proto6server/server_readdatasource_test.go +++ b/internal/proto6server/server_readdatasource_test.go @@ -286,8 +286,6 @@ func TestServerReadDataSource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_readresource.go b/internal/proto6server/server_readresource.go index d5b0cfab5..89cec5523 100644 --- a/internal/proto6server/server_readresource.go +++ b/internal/proto6server/server_readresource.go @@ -36,6 +36,14 @@ func (s *Server) ReadResource(ctx context.Context, proto6Req *tfprotov6.ReadReso return toproto6.ReadResourceResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ReadResourceResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ReadResource(ctx context.Context, proto6Req *tfprotov6.ReadReso return toproto6.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 140b4be7b..2f096bbef 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -46,6 +47,20 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov6.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testCurrentIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testProviderMetaDynamicValue := testNewDynamicValue(t, tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -70,6 +85,14 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov6.ReadResourceRequest @@ -246,6 +269,69 @@ func TestServerReadResource(t *testing.T) { }), }, }, + "request-currentidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, + }, + "test_required": metaschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -332,6 +418,55 @@ func TestServerReadResource(t *testing.T) { NewState: testNewStateDynamicValue, }, }, + "response-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + }, + }, "response-state-removeresource": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -402,8 +537,6 @@ func TestServerReadResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_renewephemeralresource_test.go b/internal/proto6server/server_renewephemeralresource_test.go index 2dfe3905b..1ed95d505 100644 --- a/internal/proto6server/server_renewephemeralresource_test.go +++ b/internal/proto6server/server_renewephemeralresource_test.go @@ -146,8 +146,6 @@ func TestServerRenewEphemeralResource(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_upgraderesourceidentity.go b/internal/proto6server/server_upgraderesourceidentity.go new file mode 100644 index 000000000..042efce8e --- /dev/null +++ b/internal/proto6server/server_upgraderesourceidentity.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// UpgradeResourceIdentity satisfies the tfprotov6.ProviderServer interface. +func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto6Req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + panic("unimplemented") // TODO:ResourceIdentity: implement +} diff --git a/internal/proto6server/server_upgraderesourcestate_test.go b/internal/proto6server/server_upgraderesourcestate_test.go index f56758075..bcc4ac145 100644 --- a/internal/proto6server/server_upgraderesourcestate_test.go +++ b/internal/proto6server/server_upgraderesourcestate_test.go @@ -267,8 +267,6 @@ func TestServerUpgradeResourceState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_validatedataresourceconfig_test.go b/internal/proto6server/server_validatedataresourceconfig_test.go index 9da96df87..163191dee 100644 --- a/internal/proto6server/server_validatedataresourceconfig_test.go +++ b/internal/proto6server/server_validatedataresourceconfig_test.go @@ -151,8 +151,6 @@ func TestServerValidateDataResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_validateephemeralresourceconfig_test.go b/internal/proto6server/server_validateephemeralresourceconfig_test.go index 9e1932143..b6ca07e44 100644 --- a/internal/proto6server/server_validateephemeralresourceconfig_test.go +++ b/internal/proto6server/server_validateephemeralresourceconfig_test.go @@ -149,8 +149,6 @@ func TestServerValidateEphemeralResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_validateproviderconfig_test.go b/internal/proto6server/server_validateproviderconfig_test.go index 5d7cc7df6..216261867 100644 --- a/internal/proto6server/server_validateproviderconfig_test.go +++ b/internal/proto6server/server_validateproviderconfig_test.go @@ -124,8 +124,6 @@ func TestServerValidateProviderConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/proto6server/server_validateresourceconfig_test.go b/internal/proto6server/server_validateresourceconfig_test.go index 11e911745..e094dd05a 100644 --- a/internal/proto6server/server_validateresourceconfig_test.go +++ b/internal/proto6server/server_validateresourceconfig_test.go @@ -8,12 +8,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateResourceConfig(t *testing.T) { @@ -149,8 +150,6 @@ func TestServerValidateResourceConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/build_value_test.go b/internal/reflect/build_value_test.go index 44d8b8f01..c943e6627 100644 --- a/internal/reflect/build_value_test.go +++ b/internal/reflect/build_value_test.go @@ -51,7 +51,6 @@ func TestBuildValue(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/helpers_test.go b/internal/reflect/helpers_test.go index ce6f35763..fcd9ee9be 100644 --- a/internal/reflect/helpers_test.go +++ b/internal/reflect/helpers_test.go @@ -237,7 +237,6 @@ func TestGetStructTags(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,7 +335,6 @@ func TestCommaSeparatedString(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() got := commaSeparatedString(test.input) @@ -360,7 +358,6 @@ func TestIsValidFieldName(t *testing.T) { "a_b": true, } for in, expected := range tests { - in, expected := in, expected t.Run(fmt.Sprintf("input=%q", in), func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/interfaces_test.go b/internal/reflect/interfaces_test.go index ae1022c59..c56f1ce96 100644 --- a/internal/reflect/interfaces_test.go +++ b/internal/reflect/interfaces_test.go @@ -260,7 +260,6 @@ func TestNewUnknownable(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -348,7 +347,6 @@ func TestFromUnknownable(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -400,7 +398,6 @@ func TestNewNullable(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -488,7 +485,6 @@ func TestFromNullable(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -550,7 +546,6 @@ func TestNewAttributeValue(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -864,7 +859,6 @@ func TestFromAttributeValue(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -919,7 +913,6 @@ func TestNewValueConverter(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -996,7 +989,6 @@ func TestFromValueCreator(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/into_test.go b/internal/reflect/into_test.go index 15e2b1048..151a8b14a 100644 --- a/internal/reflect/into_test.go +++ b/internal/reflect/into_test.go @@ -211,8 +211,6 @@ func TestInto_Slices(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/map_test.go b/internal/reflect/map_test.go index 4d57b451f..fc680841b 100644 --- a/internal/reflect/map_test.go +++ b/internal/reflect/map_test.go @@ -244,7 +244,6 @@ func TestFromMap(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/number_test.go b/internal/reflect/number_test.go index a7b8b49e2..97b6b3587 100644 --- a/internal/reflect/number_test.go +++ b/internal/reflect/number_test.go @@ -831,7 +831,6 @@ func TestFromInt(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() actualVal, diags := refl.FromInt(context.Background(), tc.typ, tc.val, path.Empty()) @@ -907,7 +906,6 @@ func TestFromUint(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() actualVal, diags := refl.FromUint(context.Background(), tc.typ, tc.val, path.Empty()) @@ -988,7 +986,6 @@ func TestFromFloat(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() actualVal, diags := refl.FromFloat(context.Background(), tc.typ, tc.val, path.Empty()) @@ -1069,7 +1066,6 @@ func TestFromBigFloat(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() actualVal, diags := refl.FromBigFloat(context.Background(), tc.typ, tc.val, path.Empty()) @@ -1145,7 +1141,6 @@ func TestFromBigInt(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() actualVal, diags := refl.FromBigInt(context.Background(), tc.typ, tc.val, path.Empty()) diff --git a/internal/reflect/outof_test.go b/internal/reflect/outof_test.go index ab618f7d0..0a24e5e7d 100644 --- a/internal/reflect/outof_test.go +++ b/internal/reflect/outof_test.go @@ -167,8 +167,6 @@ func TestFromValue_go_types(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/pointer_test.go b/internal/reflect/pointer_test.go index fdc1ddd71..e254c0fcd 100644 --- a/internal/reflect/pointer_test.go +++ b/internal/reflect/pointer_test.go @@ -160,7 +160,6 @@ func TestFromPointer(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/primitive_test.go b/internal/reflect/primitive_test.go index 729b42749..58074e2b1 100644 --- a/internal/reflect/primitive_test.go +++ b/internal/reflect/primitive_test.go @@ -136,7 +136,6 @@ func TestFromString(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,7 +212,6 @@ func TestFromBool(t *testing.T) { } for name, tc := range cases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/slice_test.go b/internal/reflect/slice_test.go index 41a8daa09..05e5fc2f4 100644 --- a/internal/reflect/slice_test.go +++ b/internal/reflect/slice_test.go @@ -388,7 +388,6 @@ func TestFromSlice(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/reflect/struct_test.go b/internal/reflect/struct_test.go index a79df6e24..fb382a180 100644 --- a/internal/reflect/struct_test.go +++ b/internal/reflect/struct_test.go @@ -395,8 +395,6 @@ func TestNewStruct_errors(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -2122,8 +2120,6 @@ func TestFromStruct_errors(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/testing/testprovider/resourcewithidentity.go b/internal/testing/testprovider/resourcewithidentity.go new file mode 100644 index 000000000..514b979d0 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentity.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentity{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentity{} + +// Declarative resource.ResourceWithIdentity for unit testing. +type ResourceWithIdentity struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentity) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/resourcewithidentityandimportstate.go b/internal/testing/testprovider/resourcewithidentityandimportstate.go new file mode 100644 index 000000000..9e26dbf7c --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentityandimportstate.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentityAndImportState{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentityAndImportState{} +var _ resource.ResourceWithImportState = &ResourceWithIdentityAndImportState{} + +// Declarative resource.ResourceWithIdentityAndImportState for unit testing. +type ResourceWithIdentityAndImportState struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) + + // ResourceWithImportState interface methods + ImportStateMethod func(context.Context, resource.ImportStateRequest, *resource.ImportStateResponse) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentityAndImportState) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} + +// ImportState satisfies the resource.ResourceWithImportState interface. +func (r *ResourceWithIdentityAndImportState) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if r.ImportStateMethod == nil { + return + } + + r.ImportStateMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/resourcewithidentityandmodifyplan.go b/internal/testing/testprovider/resourcewithidentityandmodifyplan.go new file mode 100644 index 000000000..5a1dc8847 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentityandmodifyplan.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentityAndModifyPlan{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentityAndModifyPlan{} +var _ resource.ResourceWithModifyPlan = &ResourceWithIdentityAndModifyPlan{} + +// Declarative resource.ResourceWithIdentityAndModifyPlan for unit testing. +type ResourceWithIdentityAndModifyPlan struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) + + // ResourceWithModifyPlan interface methods + ModifyPlanMethod func(context.Context, resource.ModifyPlanRequest, *resource.ModifyPlanResponse) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentityAndModifyPlan) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} + +// ModifyPlan satisfies the resource.ResourceWithModifyPlan interface. +func (r *ResourceWithIdentityAndModifyPlan) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.ModifyPlanMethod == nil { + return + } + + r.ModifyPlanMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/resourcewithidentityandmovestate.go b/internal/testing/testprovider/resourcewithidentityandmovestate.go new file mode 100644 index 000000000..60f4d0676 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentityandmovestate.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentityAndMoveState{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentityAndMoveState{} +var _ resource.ResourceWithMoveState = &ResourceWithIdentityAndMoveState{} + +// Declarative resource.ResourceWithIdentityAndMoveState for unit testing. +type ResourceWithIdentityAndMoveState struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) + + // ResourceWithMoveState interface methods + MoveStateMethod func(context.Context) []resource.StateMover +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentityAndMoveState) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} + +// MoveState satisfies the resource.ResourceWithMoveState interface. +func (r *ResourceWithIdentityAndMoveState) MoveState(ctx context.Context) []resource.StateMover { + if r.MoveStateMethod == nil { + return nil + } + + return r.MoveStateMethod(ctx) +} diff --git a/internal/testing/testschema/attribute.go b/internal/testing/testschema/attribute.go index fccc26b42..3f4e7470a 100644 --- a/internal/testing/testschema/attribute.go +++ b/internal/testing/testschema/attribute.go @@ -4,9 +4,10 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwschema.Attribute = Attribute{} @@ -19,6 +20,9 @@ type Attribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -77,3 +81,18 @@ func (a Attribute) IsRequired() bool { func (a Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithbooldefault.go b/internal/testing/testschema/attributewithbooldefault.go index 5d7b25671..e1ca0627a 100644 --- a/internal/testing/testschema/attributewithbooldefault.go +++ b/internal/testing/testschema/attributewithbooldefault.go @@ -22,6 +22,9 @@ type AttributeWithBoolDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Bool } @@ -85,3 +88,18 @@ func (a AttributeWithBoolDefaultValue) IsRequired() bool { func (a AttributeWithBoolDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go index d27661894..812697fb2 100644 --- a/internal/testing/testschema/attributewithboolplanmodifiers.go +++ b/internal/testing/testschema/attributewithboolplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolPlanModifiers = AttributeWithBoolPlanModifiers{} @@ -22,6 +23,9 @@ type AttributeWithBoolPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Bool } @@ -85,3 +89,18 @@ func (a AttributeWithBoolPlanModifiers) IsRequired() bool { func (a AttributeWithBoolPlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go index 5cc0943ca..5da143524 100644 --- a/internal/testing/testschema/attributewithboolvalidators.go +++ b/internal/testing/testschema/attributewithboolvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolValidators = AttributeWithBoolValidators{} @@ -22,6 +23,9 @@ type AttributeWithBoolValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Bool } @@ -85,3 +89,18 @@ func (a AttributeWithBoolValidators) IsRequired() bool { func (a AttributeWithBoolValidators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicdefault.go b/internal/testing/testschema/attributewithdynamicdefault.go index d366beb9a..ae3690aa0 100644 --- a/internal/testing/testschema/attributewithdynamicdefault.go +++ b/internal/testing/testschema/attributewithdynamicdefault.go @@ -22,6 +22,9 @@ type AttributeWithDynamicDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Dynamic } @@ -85,3 +88,18 @@ func (a AttributeWithDynamicDefaultValue) IsRequired() bool { func (a AttributeWithDynamicDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicplanmodifiers.go b/internal/testing/testschema/attributewithdynamicplanmodifiers.go index abb7ca6bf..f510ba12c 100644 --- a/internal/testing/testschema/attributewithdynamicplanmodifiers.go +++ b/internal/testing/testschema/attributewithdynamicplanmodifiers.go @@ -22,6 +22,9 @@ type AttributeWithDynamicPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Dynamic } @@ -85,3 +88,18 @@ func (a AttributeWithDynamicPlanModifiers) IsSensitive() bool { func (a AttributeWithDynamicPlanModifiers) DynamicPlanModifiers() []planmodifier.Dynamic { return a.PlanModifiers } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicvalidators.go b/internal/testing/testschema/attributewithdynamicvalidators.go index 1fe086775..cca5e5fc6 100644 --- a/internal/testing/testschema/attributewithdynamicvalidators.go +++ b/internal/testing/testschema/attributewithdynamicvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithDynamicValidators = AttributeWithDynamicValidators{} @@ -22,6 +23,9 @@ type AttributeWithDynamicValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Dynamic } @@ -85,3 +89,18 @@ func (a AttributeWithDynamicValidators) IsSensitive() bool { func (a AttributeWithDynamicValidators) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go index e1dabdd87..6798180f1 100644 --- a/internal/testing/testschema/attributewithfloat32default.go +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -22,6 +22,9 @@ type AttributeWithFloat32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Float32 } @@ -85,3 +88,18 @@ func (a AttributeWithFloat32DefaultValue) IsRequired() bool { func (a AttributeWithFloat32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go index b3ef10ebc..d01c9ca8d 100644 --- a/internal/testing/testschema/attributewithfloat32planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -23,6 +23,9 @@ type AttributeWithFloat32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Float32 } @@ -86,3 +89,18 @@ func (a AttributeWithFloat32PlanModifiers) IsRequired() bool { func (a AttributeWithFloat32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go index d9d38f9ea..fbf460685 100644 --- a/internal/testing/testschema/attributewithfloat32validators.go +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -23,6 +23,9 @@ type AttributeWithFloat32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Float32 } @@ -86,3 +89,18 @@ func (a AttributeWithFloat32Validators) IsRequired() bool { func (a AttributeWithFloat32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64default.go b/internal/testing/testschema/attributewithfloat64default.go index 33b717f33..9a9cca0a6 100644 --- a/internal/testing/testschema/attributewithfloat64default.go +++ b/internal/testing/testschema/attributewithfloat64default.go @@ -22,6 +22,9 @@ type AttributeWithFloat64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Float64 } @@ -85,3 +88,18 @@ func (a AttributeWithFloat64DefaultValue) IsRequired() bool { func (a AttributeWithFloat64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go index 6273cbd5d..fe9a0077e 100644 --- a/internal/testing/testschema/attributewithfloat64planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -22,6 +22,9 @@ type AttributeWithFloat64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Float64 } @@ -85,3 +88,18 @@ func (a AttributeWithFloat64PlanModifiers) IsRequired() bool { func (a AttributeWithFloat64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go index b2b2b5f70..309dbd1bc 100644 --- a/internal/testing/testschema/attributewithfloat64validators.go +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -22,6 +22,9 @@ type AttributeWithFloat64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Float64 } @@ -85,3 +88,18 @@ func (a AttributeWithFloat64Validators) IsRequired() bool { func (a AttributeWithFloat64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go index bbfe22af8..e9d6a8e9b 100644 --- a/internal/testing/testschema/attributewithint32default.go +++ b/internal/testing/testschema/attributewithint32default.go @@ -22,6 +22,9 @@ type AttributeWithInt32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Int32 } @@ -85,3 +88,18 @@ func (a AttributeWithInt32DefaultValue) IsRequired() bool { func (a AttributeWithInt32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go index aff453d9c..4cef7d2cd 100644 --- a/internal/testing/testschema/attributewithint32planmodifiers.go +++ b/internal/testing/testschema/attributewithint32planmodifiers.go @@ -23,6 +23,9 @@ type AttributeWithInt32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Int32 } @@ -86,3 +89,18 @@ func (a AttributeWithInt32PlanModifiers) IsRequired() bool { func (a AttributeWithInt32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go index 7c6913bcc..62366de90 100644 --- a/internal/testing/testschema/attributewithint32validators.go +++ b/internal/testing/testschema/attributewithint32validators.go @@ -23,6 +23,9 @@ type AttributeWithInt32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Int32 } @@ -86,3 +89,18 @@ func (a AttributeWithInt32Validators) IsRequired() bool { func (a AttributeWithInt32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64default.go b/internal/testing/testschema/attributewithint64default.go index ca9e12b96..bb3f12f00 100644 --- a/internal/testing/testschema/attributewithint64default.go +++ b/internal/testing/testschema/attributewithint64default.go @@ -22,6 +22,9 @@ type AttributeWithInt64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Int64 } @@ -85,3 +88,18 @@ func (a AttributeWithInt64DefaultValue) IsRequired() bool { func (a AttributeWithInt64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go index 368a865c0..ef315b3ae 100644 --- a/internal/testing/testschema/attributewithint64planmodifiers.go +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -22,6 +22,9 @@ type AttributeWithInt64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Int64 } @@ -85,3 +88,18 @@ func (a AttributeWithInt64PlanModifiers) IsRequired() bool { func (a AttributeWithInt64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go index 07cf28bd7..5ccb40aa7 100644 --- a/internal/testing/testschema/attributewithint64validators.go +++ b/internal/testing/testschema/attributewithint64validators.go @@ -22,6 +22,9 @@ type AttributeWithInt64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Int64 } @@ -85,3 +88,18 @@ func (a AttributeWithInt64Validators) IsRequired() bool { func (a AttributeWithInt64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistdefault.go b/internal/testing/testschema/attributewithlistdefault.go index 1f6c65d5a..3676ca7ba 100644 --- a/internal/testing/testschema/attributewithlistdefault.go +++ b/internal/testing/testschema/attributewithlistdefault.go @@ -23,6 +23,9 @@ type AttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.List } @@ -88,3 +91,18 @@ func (a AttributeWithListDefaultValue) IsRequired() bool { func (a AttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go index 08ed953fa..eafa1093b 100644 --- a/internal/testing/testschema/attributewithlistplanmodifiers.go +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -23,6 +23,9 @@ type AttributeWithListPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.List } @@ -84,7 +87,22 @@ func (a AttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a AttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go index bb47ca9d6..36182c4a0 100644 --- a/internal/testing/testschema/attributewithlistvalidators.go +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -23,6 +23,9 @@ type AttributeWithListValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.List } @@ -84,7 +87,22 @@ func (a AttributeWithListValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // ListValidators satisfies the fwxschema.AttributeWithListValidators interface. func (a AttributeWithListValidators) ListValidators() []validator.List { return a.Validators } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapdefault.go b/internal/testing/testschema/attributewithmapdefault.go index a8bf1910d..0ca52972c 100644 --- a/internal/testing/testschema/attributewithmapdefault.go +++ b/internal/testing/testschema/attributewithmapdefault.go @@ -23,6 +23,9 @@ type AttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Map } @@ -88,3 +91,18 @@ func (a AttributeWithMapDefaultValue) IsRequired() bool { func (a AttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go index 1d067e1d4..b57d597ce 100644 --- a/internal/testing/testschema/attributewithmapplanmodifiers.go +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -23,6 +23,9 @@ type AttributeWithMapPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Map } @@ -84,7 +87,22 @@ func (a AttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a AttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go index 1469ceee2..8b2ec6b14 100644 --- a/internal/testing/testschema/attributewithmapvalidators.go +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -23,6 +23,9 @@ type AttributeWithMapValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Map } @@ -84,6 +87,21 @@ func (a AttributeWithMapValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapValidators satisfies the fwxschema.AttributeWithMapValidators interface. func (a AttributeWithMapValidators) MapValidators() []validator.Map { return a.Validators diff --git a/internal/testing/testschema/attributewithnumberdefault.go b/internal/testing/testschema/attributewithnumberdefault.go index 7bca51169..bdb16149d 100644 --- a/internal/testing/testschema/attributewithnumberdefault.go +++ b/internal/testing/testschema/attributewithnumberdefault.go @@ -22,6 +22,9 @@ type AttributeWithNumberDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Number } @@ -85,3 +88,18 @@ func (a AttributeWithNumberDefaultValue) IsRequired() bool { func (a AttributeWithNumberDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go index 7073a1e72..db6bf3d46 100644 --- a/internal/testing/testschema/attributewithnumberplanmodifiers.go +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -22,6 +22,9 @@ type AttributeWithNumberPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Number } @@ -81,6 +84,21 @@ func (a AttributeWithNumberPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // NumberPlanModifiers satisfies the fwxschema.AttributeWithNumberPlanModifiers interface. func (a AttributeWithNumberPlanModifiers) NumberPlanModifiers() []planmodifier.Number { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithnumbervalidators.go b/internal/testing/testschema/attributewithnumbervalidators.go index 0e63b7db3..d5ba9bc7c 100644 --- a/internal/testing/testschema/attributewithnumbervalidators.go +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -22,6 +22,9 @@ type AttributeWithNumberValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Number } @@ -81,6 +84,21 @@ func (a AttributeWithNumberValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // NumberValidators satisfies the fwxschema.AttributeWithNumberValidators interface. func (a AttributeWithNumberValidators) NumberValidators() []validator.Number { return a.Validators diff --git a/internal/testing/testschema/attributewithobjectdefault.go b/internal/testing/testschema/attributewithobjectdefault.go index 54e0e594e..c8ae9e047 100644 --- a/internal/testing/testschema/attributewithobjectdefault.go +++ b/internal/testing/testschema/attributewithobjectdefault.go @@ -23,6 +23,9 @@ type AttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Object } @@ -88,3 +91,18 @@ func (a AttributeWithObjectDefaultValue) IsRequired() bool { func (a AttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go index 87f5933e2..625746b47 100644 --- a/internal/testing/testschema/attributewithobjectplanmodifiers.go +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -23,6 +23,9 @@ type AttributeWithObjectPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Object } @@ -84,6 +87,21 @@ func (a AttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a AttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithobjectvalidators.go b/internal/testing/testschema/attributewithobjectvalidators.go index 854be0dbd..03bab115f 100644 --- a/internal/testing/testschema/attributewithobjectvalidators.go +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -23,6 +23,9 @@ type AttributeWithObjectValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Object } @@ -84,6 +87,21 @@ func (a AttributeWithObjectValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectValidators satisfies the fwxschema.AttributeWithObjectValidators interface. func (a AttributeWithObjectValidators) ObjectValidators() []validator.Object { return a.Validators diff --git a/internal/testing/testschema/attributewithsetdefault.go b/internal/testing/testschema/attributewithsetdefault.go index 351ce17b8..d5d557cec 100644 --- a/internal/testing/testschema/attributewithsetdefault.go +++ b/internal/testing/testschema/attributewithsetdefault.go @@ -23,6 +23,9 @@ type AttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Set } @@ -88,3 +91,18 @@ func (a AttributeWithSetDefaultValue) IsRequired() bool { func (a AttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go index 4efb38ac4..685709121 100644 --- a/internal/testing/testschema/attributewithsetplanmodifiers.go +++ b/internal/testing/testschema/attributewithsetplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithSetPlanModifiers = AttributeWithSetPlanModifiers{} @@ -23,6 +24,9 @@ type AttributeWithSetPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Set } @@ -84,6 +88,21 @@ func (a AttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a AttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithsetvalidators.go b/internal/testing/testschema/attributewithsetvalidators.go index 217b11b21..ed18808cc 100644 --- a/internal/testing/testschema/attributewithsetvalidators.go +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -23,6 +23,9 @@ type AttributeWithSetValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Set } @@ -84,6 +87,21 @@ func (a AttributeWithSetValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetValidators satisfies the fwxschema.AttributeWithSetValidators interface. func (a AttributeWithSetValidators) SetValidators() []validator.Set { return a.Validators diff --git a/internal/testing/testschema/attributewithstringdefault.go b/internal/testing/testschema/attributewithstringdefault.go index 88e93f004..28131c39a 100644 --- a/internal/testing/testschema/attributewithstringdefault.go +++ b/internal/testing/testschema/attributewithstringdefault.go @@ -22,6 +22,9 @@ type AttributeWithStringDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.String } @@ -85,3 +88,18 @@ func (a AttributeWithStringDefaultValue) IsRequired() bool { func (a AttributeWithStringDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go index cbf324d66..f88a93cf7 100644 --- a/internal/testing/testschema/attributewithstringplanmodifiers.go +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -22,6 +22,9 @@ type AttributeWithStringPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.String } @@ -81,6 +84,21 @@ func (a AttributeWithStringPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // StringPlanModifiers satisfies the fwxschema.AttributeWithStringPlanModifiers interface. func (a AttributeWithStringPlanModifiers) StringPlanModifiers() []planmodifier.String { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithstringvalidators.go b/internal/testing/testschema/attributewithstringvalidators.go index a864dd314..21efefe3d 100644 --- a/internal/testing/testschema/attributewithstringvalidators.go +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -22,6 +22,9 @@ type AttributeWithStringValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.String } @@ -81,6 +84,21 @@ func (a AttributeWithStringValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // StringValidators satisfies the fwxschema.AttributeWithStringValidators interface. func (a AttributeWithStringValidators) StringValidators() []validator.String { return a.Validators diff --git a/internal/testing/testschema/nested_attribute.go b/internal/testing/testschema/nested_attribute.go index f078f7cb9..2a638bcb4 100644 --- a/internal/testing/testschema/nested_attribute.go +++ b/internal/testing/testschema/nested_attribute.go @@ -27,6 +27,9 @@ type NestedAttribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -157,3 +160,18 @@ func (a NestedAttribute) IsRequired() bool { func (a NestedAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/nested_attribute_with_list_default.go b/internal/testing/testschema/nested_attribute_with_list_default.go index 0980629cd..2621e9d39 100644 --- a/internal/testing/testschema/nested_attribute_with_list_default.go +++ b/internal/testing/testschema/nested_attribute_with_list_default.go @@ -27,6 +27,9 @@ type NestedAttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ListDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithListDefaultValue) ListDefaultValue() defaults.List { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go index e20d89df8..e754a7a6d 100644 --- a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go @@ -27,6 +27,9 @@ type NestedAttributeWithListPlanModifiers struct { PlanModifiers []planmodifier.List Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a NestedAttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_map_default.go b/internal/testing/testschema/nested_attribute_with_map_default.go index 128ae137c..be51d0a36 100644 --- a/internal/testing/testschema/nested_attribute_with_map_default.go +++ b/internal/testing/testschema/nested_attribute_with_map_default.go @@ -27,6 +27,9 @@ type NestedAttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithMapDefaultValue) MapDefaultValue() defaults.Map { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go index 27ba35b45..855ef49df 100644 --- a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go @@ -27,6 +27,9 @@ type NestedAttributeWithMapPlanModifiers struct { PlanModifiers []planmodifier.Map Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a NestedAttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_object_default.go b/internal/testing/testschema/nested_attribute_with_object_default.go index 0f6a96489..5d83fb9d9 100644 --- a/internal/testing/testschema/nested_attribute_with_object_default.go +++ b/internal/testing/testschema/nested_attribute_with_object_default.go @@ -28,6 +28,9 @@ type NestedAttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -105,6 +108,21 @@ func (a NestedAttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithObjectDefaultValue) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go index 00f303165..5f70c1fda 100644 --- a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go @@ -26,6 +26,9 @@ type NestedAttributeWithObjectPlanModifiers struct { PlanModifiers []planmodifier.Object Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -99,6 +102,21 @@ func (a NestedAttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a NestedAttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_set_default.go b/internal/testing/testschema/nested_attribute_with_set_default.go index a8f39e9a6..16fc98d1c 100644 --- a/internal/testing/testschema/nested_attribute_with_set_default.go +++ b/internal/testing/testschema/nested_attribute_with_set_default.go @@ -27,6 +27,9 @@ type NestedAttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithSetDefaultValue) SetDefaultValue() defaults.Set { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go index 6c988d86a..a92acd392 100644 --- a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go @@ -27,6 +27,9 @@ type NestedAttributeWithSetPlanModifiers struct { PlanModifiers []planmodifier.Set Required bool Sensitive bool + WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -102,6 +105,21 @@ func (a NestedAttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a NestedAttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/testing/testtypes/stringwithsemanticequals.go b/internal/testing/testtypes/stringwithsemanticequals.go index 4baf39d4c..a98c06a24 100644 --- a/internal/testing/testtypes/stringwithsemanticequals.go +++ b/internal/testing/testtypes/stringwithsemanticequals.go @@ -24,7 +24,12 @@ var ( type StringTypeWithSemanticEquals struct { basetypes.StringType - SemanticEquals bool + // Will always return this boolean for semantic equality + SemanticEquals bool + + // Will only return semantic equality as true if the attr.Value matches this + SemanticallyEqualTo attr.Value + SemanticEqualsDiagnostics diag.Diagnostics } @@ -52,6 +57,7 @@ func (t StringTypeWithSemanticEquals) ValueFromString(ctx context.Context, in ba value := StringValueWithSemanticEquals{ StringValue: in, SemanticEquals: t.SemanticEquals, + SemanticallyEqualTo: t.SemanticallyEqualTo, SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, } @@ -83,6 +89,7 @@ func (t StringTypeWithSemanticEquals) ValueFromTerraform(ctx context.Context, in func (t StringTypeWithSemanticEquals) ValueType(ctx context.Context) attr.Value { return StringValueWithSemanticEquals{ SemanticEquals: t.SemanticEquals, + SemanticallyEqualTo: t.SemanticallyEqualTo, SemanticEqualsDiagnostics: t.SemanticEqualsDiagnostics, } } @@ -90,7 +97,12 @@ func (t StringTypeWithSemanticEquals) ValueType(ctx context.Context) attr.Value type StringValueWithSemanticEquals struct { basetypes.StringValue - SemanticEquals bool + // Will always return this boolean for semantic equality + SemanticEquals bool + + // Will only return semantic equality as true if the attr.Value matches this + SemanticallyEqualTo attr.Value + SemanticEqualsDiagnostics diag.Diagnostics } @@ -105,6 +117,9 @@ func (v StringValueWithSemanticEquals) Equal(o attr.Value) bool { } func (v StringValueWithSemanticEquals) StringSemanticEquals(ctx context.Context, otherV basetypes.StringValuable) (bool, diag.Diagnostics) { + if v.SemanticallyEqualTo != nil && !v.SemanticallyEqualTo.IsNull() { + return v.SemanticallyEqualTo.Equal(otherV), v.SemanticEqualsDiagnostics + } return v.SemanticEquals, v.SemanticEqualsDiagnostics } diff --git a/internal/toproto5/applyresourcechange.go b/internal/toproto5/applyresourcechange.go index 289373999..1b5da0df6 100644 --- a/internal/toproto5/applyresourcechange.go +++ b/internal/toproto5/applyresourcechange.go @@ -27,6 +27,11 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/applyresourcechange_test.go b/internal/toproto5/applyresourcechange_test.go index 64c84f7cc..63911e17c 100644 --- a/internal/toproto5/applyresourcechange_test.go +++ b/internal/toproto5/applyresourcechange_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -38,6 +39,22 @@ func TestApplyResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto5Value, Schema: schema.Schema{ @@ -60,6 +77,28 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -131,6 +170,37 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ApplyResourceChangeResponse{ NewState: testState, @@ -139,6 +209,16 @@ func TestApplyResourceChangeResponse(t *testing.T) { NewState: &testProto5DynamicValue, }, }, + "newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "private": { input: &fwserver.ApplyResourceChangeResponse{ Private: &privatestate.Data{ @@ -158,8 +238,6 @@ func TestApplyResourceChangeResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/block_test.go b/internal/toproto5/block_test.go index 4618d862c..303474e56 100644 --- a/internal/toproto5/block_test.go +++ b/internal/toproto5/block_test.go @@ -551,7 +551,6 @@ func TestBlock(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/callfunction_test.go b/internal/toproto5/callfunction_test.go index 91c26725b..7c7f49a36 100644 --- a/internal/toproto5/callfunction_test.go +++ b/internal/toproto5/callfunction_test.go @@ -53,8 +53,6 @@ func TestCallFunctionResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/closeephemeralresource_test.go b/internal/toproto5/closeephemeralresource_test.go index f10ab23b1..12b2e1171 100644 --- a/internal/toproto5/closeephemeralresource_test.go +++ b/internal/toproto5/closeephemeralresource_test.go @@ -54,8 +54,6 @@ func TestCloseEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/config_test.go b/internal/toproto5/config_test.go index 5cff0d303..2574bcf6e 100644 --- a/internal/toproto5/config_test.go +++ b/internal/toproto5/config_test.go @@ -90,8 +90,6 @@ func TestConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/configureprovider_test.go b/internal/toproto5/configureprovider_test.go index 5ecdf930f..2d7a20d68 100644 --- a/internal/toproto5/configureprovider_test.go +++ b/internal/toproto5/configureprovider_test.go @@ -54,8 +54,6 @@ func TestConfigureProviderResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/datasourcemetadata_test.go b/internal/toproto5/datasourcemetadata_test.go index 882de7469..10324596e 100644 --- a/internal/toproto5/datasourcemetadata_test.go +++ b/internal/toproto5/datasourcemetadata_test.go @@ -31,8 +31,6 @@ func TestDataSourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/diagnostics_test.go b/internal/toproto5/diagnostics_test.go index 7e888cb19..f10c2c641 100644 --- a/internal/toproto5/diagnostics_test.go +++ b/internal/toproto5/diagnostics_test.go @@ -38,8 +38,6 @@ func TestDiagnosticSeverity(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -116,7 +114,6 @@ func TestDiagnostics(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/dynamic_value_test.go b/internal/toproto5/dynamic_value_test.go index 613a377f2..ee9dd1f69 100644 --- a/internal/toproto5/dynamic_value_test.go +++ b/internal/toproto5/dynamic_value_test.go @@ -1230,8 +1230,6 @@ func TestDynamicValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/ephemeral_result_data_test.go b/internal/toproto5/ephemeral_result_data_test.go index bcb082b21..4a303f016 100644 --- a/internal/toproto5/ephemeral_result_data_test.go +++ b/internal/toproto5/ephemeral_result_data_test.go @@ -90,8 +90,6 @@ func TestEphemeralResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/ephemeralresourcemetadata_test.go b/internal/toproto5/ephemeralresourcemetadata_test.go index 2e3e13731..81f6566d7 100644 --- a/internal/toproto5/ephemeralresourcemetadata_test.go +++ b/internal/toproto5/ephemeralresourcemetadata_test.go @@ -31,8 +31,6 @@ func TestEphemeralResourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/function_test.go b/internal/toproto5/function_test.go index 8f941c805..f29d0a9bb 100644 --- a/internal/toproto5/function_test.go +++ b/internal/toproto5/function_test.go @@ -215,8 +215,6 @@ func TestFunction(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +245,6 @@ func TestFunctionMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -465,8 +461,6 @@ func TestFunctionParameter(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -499,8 +493,6 @@ func TestFunctionReturn(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -539,8 +531,6 @@ func TestFunctionResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/getfunctions_test.go b/internal/toproto5/getfunctions_test.go index f90ed5a7b..844c03a38 100644 --- a/internal/toproto5/getfunctions_test.go +++ b/internal/toproto5/getfunctions_test.go @@ -221,8 +221,6 @@ func TestGetFunctionsResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go index f328d387e..a2d0c2bdc 100644 --- a/internal/toproto5/getmetadata_test.go +++ b/internal/toproto5/getmetadata_test.go @@ -173,8 +173,6 @@ func TestGetMetadataResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 7a6cd4761..ca293fa96 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1338,6 +1370,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "ephemeral-resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "ephemeral-resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ EphemeralResourceSchemas: map[string]fwschema.Schema{ @@ -2461,6 +2525,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4042,6 +4135,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Optional: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ @@ -4884,8 +5010,6 @@ func TestGetProviderSchemaResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/getresourceidentityschemas.go b/internal/toproto5/getresourceidentityschemas.go new file mode 100644 index 000000000..4080a1710 --- /dev/null +++ b/internal/toproto5/getresourceidentityschemas.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemasResponse returns the *tfprotov5.GetResourceIdentitySchemasResponse +// equivalent of a *fwserver.GetResourceIdentitySchemasResponse. +func GetResourceIdentitySchemasResponse(ctx context.Context, fw *fwserver.GetResourceIdentitySchemasResponse) *tfprotov5.GetResourceIdentitySchemasResponse { + if fw == nil { + return nil + } + + protov5 := &tfprotov5.GetResourceIdentitySchemasResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + IdentitySchemas: make(map[string]*tfprotov5.ResourceIdentitySchema, len(fw.IdentitySchemas)), + } + + var err error + + for resourceType, identitySchema := range fw.IdentitySchemas { + protov5.IdentitySchemas[resourceType], err = IdentitySchema(ctx, identitySchema) + + if err != nil { + protov5.Diagnostics = append(protov5.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting resource identity schema", + Detail: "The identity schema for the resource \"" + resourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov5 + } + } + + return protov5 +} diff --git a/internal/toproto5/getresourceidentityschemas_test.go b/internal/toproto5/getresourceidentityschemas_test.go new file mode 100644 index 000000000..0dff87452 --- /dev/null +++ b/internal/toproto5/getresourceidentityschemas_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestGetResourceIdentitySchemasResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetResourceIdentitySchemasResponse + expected *tfprotov5.GetResourceIdentitySchemasResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "resource-identity-identity-multiple-resources": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource_1": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource_2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource_1": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + "test_resource_2": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-optionalforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + OptionalForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-requiredforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-bool": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-list-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-number": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.NumberAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + }, + "resource-identity-version": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Version: 123, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{}, + Version: 123, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.GetResourceIdentitySchemasResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/identity_schema.go b/internal/toproto5/identity_schema.go new file mode 100644 index 000000000..003dbf155 --- /dev/null +++ b/internal/toproto5/identity_schema.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema returns the *tfprotov5.ResourceIdentitySchema equivalent of a Schema. +func IdentitySchema(ctx context.Context, s fwschema.Schema) (*tfprotov5.ResourceIdentitySchema, error) { + if s == nil { + return nil, nil + } + + result := &tfprotov5.ResourceIdentitySchema{ + Version: s.GetVersion(), + } + + attrs := make([]*tfprotov5.ResourceIdentitySchemaAttribute, 0) + + for name, attr := range s.GetAttributes() { + a, err := IdentitySchemaAttribute(ctx, name, tftypes.NewAttributePath().WithAttributeName(name), attr) + + if err != nil { + return nil, err + } + + attrs = append(attrs, a) + } + + sort.Slice(attrs, func(i, j int) bool { + if attrs[i] == nil { + return true + } + + if attrs[j] == nil { + return false + } + + return attrs[i].Name < attrs[j].Name + }) + + result.IdentityAttributes = attrs + + return result, nil +} diff --git a/internal/toproto5/identity_schema_attribute.go b/internal/toproto5/identity_schema_attribute.go new file mode 100644 index 000000000..01714f7a3 --- /dev/null +++ b/internal/toproto5/identity_schema_attribute.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// IdentitySchemaAttribute returns the *tfprotov5.ResourceIdentitySchemaAttribute equivalent of an +// Attribute. Errors will be tftypes.AttributePathErrors based on `path`. `name` is the name of the attribute. +func IdentitySchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath, a fwschema.Attribute) (*tfprotov5.ResourceIdentitySchemaAttribute, error) { + if _, ok := a.(fwschema.NestedAttribute); ok { + return nil, path.NewErrorf("identity schemas and protocol version 5 don't support NestedAttribute") + } + + if a.GetType() == nil { + return nil, path.NewErrorf("must have Type set") + } + + if !a.IsRequiredForImport() && !a.IsOptionalForImport() { + return nil, path.NewErrorf("must have RequiredForImport or OptionalForImport set") + } + + identitySchemaAttribute := &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: name, + RequiredForImport: a.IsRequiredForImport(), + OptionalForImport: a.IsOptionalForImport(), + Type: a.GetType().TerraformType(ctx), + + // Unlike other schema attributes, identity attributes only have a single description field which + // is assumed to be markdown. Both a.GetDescription() and a.GetMarkdownDescription() will return + // the same string, so we just chose one here. + Description: a.GetDescription(), + } + + return identitySchemaAttribute, nil +} diff --git a/internal/toproto5/identity_schema_attribute_test.go b/internal/toproto5/identity_schema_attribute_test.go new file mode 100644 index 000000000..28704ba5d --- /dev/null +++ b/internal/toproto5/identity_schema_attribute_test.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchemaAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr fwschema.Attribute + path *tftypes.AttributePath + expected *tfprotov5.ResourceIdentitySchemaAttribute + expectedErr string + } + + tests := map[string]testCase{ + "description": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + Description: "A string attribute", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + Description: "A string attribute", + }, + }, + "attr-string": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "attr-bool": { + name: "bool", + attr: testschema.Attribute{ + Type: types.BoolType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + "attr-number": { + name: "number", + attr: testschema.Attribute{ + Type: types.NumberType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "number", + Type: tftypes.Number, + RequiredForImport: true, + }, + }, + "attr-list": { + name: "list", + attr: testschema.Attribute{ + Type: types.ListType{ElemType: types.NumberType}, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "list", + Type: tftypes.List{ElementType: tftypes.Number}, + RequiredForImport: true, + }, + }, + "requiredforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "optionalforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + "nested-attr-single-error": { + name: "single_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-list-error": { + name: "list_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeList, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-map-error": { + name: "map_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeMap, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-set-error": { + name: "set_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSet, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "attr-unset": { + name: "whoops", + attr: testschema.Attribute{ + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have Type set", + }, + "missing-requiredforimport-and-optionalforimport": { + name: "whoops", + attr: testschema.Attribute{ + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have RequiredForImport or OptionalForImport set", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto5.IdentitySchemaAttribute(context.Background(), tc.name, tc.path, tc.attr) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto5/identity_schema_test.go b/internal/toproto5/identity_schema_test.go new file mode 100644 index 000000000..8388c4c16 --- /dev/null +++ b/internal/toproto5/identity_schema_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input fwschema.Schema + expected *tfprotov5.ResourceIdentitySchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "empty-val": { + input: testschema.Schema{}, + expected: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{}, + Version: 0, + }, + }, + "basic-attrs": { + input: testschema.Schema{ + Version: 1, + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "number": testschema.Attribute{ + Type: types.NumberType, + OptionalForImport: true, + }, + "bool": testschema.Attribute{ + Type: types.BoolType, + OptionalForImport: true, + }, + }, + }, + expected: &tfprotov5.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + "complex-attrs": { + input: testschema.Schema{ + Version: 2, + Attributes: map[string]fwschema.Attribute{ + "list_of_string": testschema.Attribute{ + Type: types.ListType{ElemType: types.StringType}, + RequiredForImport: true, + }, + "list_of_bool": testschema.Attribute{ + Type: types.ListType{ElemType: types.BoolType}, + RequiredForImport: true, + }, + }, + }, + expected: &tfprotov5.ResourceIdentitySchema{ + Version: 2, + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bool", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + { + Name: "list_of_string", + Type: tftypes.List{ElementType: tftypes.String}, + RequiredForImport: true, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto5.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto5/importedresource.go b/internal/toproto5/importedresource.go index 1a208db64..0b515dbd7 100644 --- a/internal/toproto5/importedresource.go +++ b/internal/toproto5/importedresource.go @@ -27,6 +27,11 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr proto5.State = state + identity, identityDiags := ResourceIdentity(ctx, fw.Identity) + + diags = append(diags, identityDiags...) + proto5.Identity = identity + newPrivate, privateDiags := fw.Private.Bytes(ctx) diags = append(diags, privateDiags...) diff --git a/internal/toproto5/importedresource_test.go b/internal/toproto5/importedresource_test.go index a1e9e4146..4d2ef533b 100644 --- a/internal/toproto5/importedresource_test.go +++ b/internal/toproto5/importedresource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -50,6 +51,22 @@ func TestImportResourceStateResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testState := tfsdk.State{ Raw: testProto5Value, Schema: schema.Schema{ @@ -85,6 +102,28 @@ func TestImportResourceStateResponse(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.ImportResourceStateResponse expected *tfprotov5.ImportResourceStateResponse @@ -154,6 +193,42 @@ func TestImportResourceStateResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-identity": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + Identity: testIdentityInvalid, + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ImportResourceStateResponse{ ImportedResources: []fwserver.ImportedResource{ @@ -170,6 +245,26 @@ func TestImportResourceStateResponse(t *testing.T) { }, }, }, + "identity": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + Identity: testIdentity, + }, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + State: &testProto5DynamicValue, + Identity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, + }, + }, "private": { input: &fwserver.ImportResourceStateResponse{ ImportedResources: []fwserver.ImportedResource{ @@ -199,8 +294,6 @@ func TestImportResourceStateResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/moveresourcestate.go b/internal/toproto5/moveresourcestate.go index bfffadf1b..2c598c46c 100644 --- a/internal/toproto5/moveresourcestate.go +++ b/internal/toproto5/moveresourcestate.go @@ -27,6 +27,11 @@ func MoveResourceStateResponse(ctx context.Context, fw *fwserver.MoveResourceSta proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.TargetPrivate = targetPrivate + targetIdentity, diags := ResourceIdentity(ctx, fw.TargetIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.TargetIdentity = targetIdentity + targetState, diags := State(ctx, fw.TargetState) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/moveresourcestate_test.go b/internal/toproto5/moveresourcestate_test.go index 76712ee64..948ec6150 100644 --- a/internal/toproto5/moveresourcestate_test.go +++ b/internal/toproto5/moveresourcestate_test.go @@ -5,6 +5,7 @@ package toproto5_test import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" @@ -33,6 +34,44 @@ func TestMoveResourceStateResponse(t *testing.T) { }, ) + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.MoveResourceStateResponse expected *tfprotov5.MoveResourceStateResponse @@ -152,11 +191,50 @@ func TestMoveResourceStateResponse(t *testing.T) { }, }, }, + "TargetIdentity": { + input: &fwserver.MoveResourceStateResponse{ + TargetIdentity: testIdentity, + }, + expected: &tfprotov5.MoveResourceStateResponse{ + TargetIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, + "TargetIdentity-invalid": { + input: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + TargetIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.MoveResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/openephemeralresource_test.go b/internal/toproto5/openephemeralresource_test.go index 582a1f90e..fe6cf04c4 100644 --- a/internal/toproto5/openephemeralresource_test.go +++ b/internal/toproto5/openephemeralresource_test.go @@ -199,8 +199,6 @@ func TestOpenEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index f3292a963..23938df27 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -29,6 +29,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.PlannedState = plannedState + plannedIdentity, diags := ResourceIdentity(ctx, fw.PlannedIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.PlannedIdentity = plannedIdentity + requiresReplace, diags := totftypes.AttributePaths(ctx, fw.RequiresReplace) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index d4ed9e674..9506ba513 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -40,6 +41,22 @@ func TestPlanResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto5Value, Schema: schema.Schema{ @@ -62,6 +79,28 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -143,6 +182,37 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + PlannedIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "plannedprivate-empty": { input: &fwserver.PlanResourceChangeResponse{ PlannedPrivate: &privatestate.Data{ @@ -177,6 +247,16 @@ func TestPlanResourceChangeResponse(t *testing.T) { PlannedState: &testProto5DynamicValue, }, }, + "plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: testIdentity, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "requiresreplace": { input: &fwserver.PlanResourceChangeResponse{ RequiresReplace: path.Paths{ @@ -200,8 +280,6 @@ func TestPlanResourceChangeResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/prepareproviderconfig_test.go b/internal/toproto5/prepareproviderconfig_test.go index 46f5ea451..67668e5e4 100644 --- a/internal/toproto5/prepareproviderconfig_test.go +++ b/internal/toproto5/prepareproviderconfig_test.go @@ -134,8 +134,6 @@ func TestPrepareProviderConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/readdatasource_test.go b/internal/toproto5/readdatasource_test.go index bc39fd51a..8801eefe9 100644 --- a/internal/toproto5/readdatasource_test.go +++ b/internal/toproto5/readdatasource_test.go @@ -152,8 +152,6 @@ func TestReadDataSourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index 9193a3950..56695adae 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -28,6 +28,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 582e7090a..8058f1c38 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -39,6 +40,22 @@ func TestReadResourceResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -69,6 +86,28 @@ func TestReadResourceResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testDeferral := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } @@ -142,6 +181,37 @@ func TestReadResourceResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.ReadResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ReadResourceResponse{ NewState: testState, @@ -150,6 +220,16 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto5DynamicValue, }, }, + "newidentity": { + input: &fwserver.ReadResourceResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov5.ReadResourceResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "private-empty": { input: &fwserver.ReadResourceResponse{ Private: &privatestate.Data{ @@ -187,8 +267,6 @@ func TestReadResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/renewephemeralresource_test.go b/internal/toproto5/renewephemeralresource_test.go index 975e297c8..a1c55bf03 100644 --- a/internal/toproto5/renewephemeralresource_test.go +++ b/internal/toproto5/renewephemeralresource_test.go @@ -102,8 +102,6 @@ func TestRenewEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/resource_identity.go b/internal/toproto5/resource_identity.go new file mode 100644 index 000000000..eea046ac9 --- /dev/null +++ b/internal/toproto5/resource_identity.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ResourceIdentity returns the *tfprotov5.ResourceIdentityData for a *tfsdk.ResourceIdentity. +func ResourceIdentity(ctx context.Context, fw *tfsdk.ResourceIdentity) (*tfprotov5.ResourceIdentityData, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + identitySchemaData := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + identityData, diags := DynamicValue(ctx, identitySchemaData) + if diags.HasError() { + return nil, diags + } + + return &tfprotov5.ResourceIdentityData{ + IdentityData: identityData, + }, nil +} diff --git a/internal/toproto5/resource_identity_test.go b/internal/toproto5/resource_identity_test.go new file mode 100644 index 000000000..ff62eff67 --- /dev/null +++ b/internal/toproto5/resource_identity_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testResourceIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.ResourceIdentity + expected *tfprotov5.ResourceIdentityData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testResourceIdentityInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testResourceIdentity, + expected: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto5.ResourceIdentity(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/resourcemetadata_test.go b/internal/toproto5/resourcemetadata_test.go index d8572f227..a8a982977 100644 --- a/internal/toproto5/resourcemetadata_test.go +++ b/internal/toproto5/resourcemetadata_test.go @@ -31,8 +31,6 @@ func TestResourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/schema_attribute.go b/internal/toproto5/schema_attribute.go index 74d8fa551..c9bc37e3a 100644 --- a/internal/toproto5/schema_attribute.go +++ b/internal/toproto5/schema_attribute.go @@ -6,9 +6,10 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov5.SchemaAttribute equivalent of an @@ -34,6 +35,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { diff --git a/internal/toproto5/schema_attribute_test.go b/internal/toproto5/schema_attribute_test.go index 9b49ca736..cc52ebd92 100644 --- a/internal/toproto5/schema_attribute_test.go +++ b/internal/toproto5/schema_attribute_test.go @@ -369,7 +369,6 @@ func TestSchemaAttribute(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/schema_test.go b/internal/toproto5/schema_test.go index 30a0e82f1..587f0424c 100644 --- a/internal/toproto5/schema_test.go +++ b/internal/toproto5/schema_test.go @@ -523,7 +523,6 @@ func TestSchema(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/server_capabilities_test.go b/internal/toproto5/server_capabilities_test.go index 95eac4af6..bf78fd7d4 100644 --- a/internal/toproto5/server_capabilities_test.go +++ b/internal/toproto5/server_capabilities_test.go @@ -51,8 +51,6 @@ func TestServerCapabilities(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/state_test.go b/internal/toproto5/state_test.go index b9b514308..355ca35ce 100644 --- a/internal/toproto5/state_test.go +++ b/internal/toproto5/state_test.go @@ -90,8 +90,6 @@ func TestState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/upgraderesourcestate_test.go b/internal/toproto5/upgraderesourcestate_test.go index 42ff28515..d29b2f395 100644 --- a/internal/toproto5/upgraderesourcestate_test.go +++ b/internal/toproto5/upgraderesourcestate_test.go @@ -134,8 +134,6 @@ func TestUpgradeResourceStateResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/validatedatasourceconfig_test.go b/internal/toproto5/validatedatasourceconfig_test.go index fe9001f84..7e0514a6f 100644 --- a/internal/toproto5/validatedatasourceconfig_test.go +++ b/internal/toproto5/validatedatasourceconfig_test.go @@ -54,8 +54,6 @@ func TestValidateDataSourceConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/validateephemeralresourceconfig_test.go b/internal/toproto5/validateephemeralresourceconfig_test.go index 0a2a939f3..a3c4c7dd6 100644 --- a/internal/toproto5/validateephemeralresourceconfig_test.go +++ b/internal/toproto5/validateephemeralresourceconfig_test.go @@ -54,8 +54,6 @@ func TestValidateEphemeralResourceConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto5/validateresourcetypeconfig_test.go b/internal/toproto5/validateresourcetypeconfig_test.go index 820a7f59a..8596f3f40 100644 --- a/internal/toproto5/validateresourcetypeconfig_test.go +++ b/internal/toproto5/validateresourcetypeconfig_test.go @@ -54,8 +54,6 @@ func TestValidateResourceTypeConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/applyresourcechange.go b/internal/toproto6/applyresourcechange.go index c3d158f0a..d47230d46 100644 --- a/internal/toproto6/applyresourcechange.go +++ b/internal/toproto6/applyresourcechange.go @@ -27,6 +27,11 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/applyresourcechange_test.go b/internal/toproto6/applyresourcechange_test.go index 9a4d1f886..19a9f6144 100644 --- a/internal/toproto6/applyresourcechange_test.go +++ b/internal/toproto6/applyresourcechange_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -38,6 +39,22 @@ func TestApplyResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -60,6 +77,28 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -131,6 +170,37 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ApplyResourceChangeResponse{ NewState: testState, @@ -139,6 +209,16 @@ func TestApplyResourceChangeResponse(t *testing.T) { NewState: &testProto6DynamicValue, }, }, + "newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov6.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "private": { input: &fwserver.ApplyResourceChangeResponse{ Private: &privatestate.Data{ @@ -157,8 +237,6 @@ func TestApplyResourceChangeResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/block_test.go b/internal/toproto6/block_test.go index 37369b503..e1c5784d3 100644 --- a/internal/toproto6/block_test.go +++ b/internal/toproto6/block_test.go @@ -551,7 +551,6 @@ func TestBlock(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/callfunction_test.go b/internal/toproto6/callfunction_test.go index 2f8df94d0..b7757a2eb 100644 --- a/internal/toproto6/callfunction_test.go +++ b/internal/toproto6/callfunction_test.go @@ -53,8 +53,6 @@ func TestCallFunctionResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/closeephemeralresource_test.go b/internal/toproto6/closeephemeralresource_test.go index cf9830dd0..05bd2cd1b 100644 --- a/internal/toproto6/closeephemeralresource_test.go +++ b/internal/toproto6/closeephemeralresource_test.go @@ -54,8 +54,6 @@ func TestCloseEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/config_test.go b/internal/toproto6/config_test.go index e8543200a..4cc350877 100644 --- a/internal/toproto6/config_test.go +++ b/internal/toproto6/config_test.go @@ -90,8 +90,6 @@ func TestConfig(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/configureprovider_test.go b/internal/toproto6/configureprovider_test.go index f8f999dbd..13b28959a 100644 --- a/internal/toproto6/configureprovider_test.go +++ b/internal/toproto6/configureprovider_test.go @@ -54,8 +54,6 @@ func TestConfigureProviderResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/datasourcemetadata_test.go b/internal/toproto6/datasourcemetadata_test.go index af81c1f80..583a8e7a1 100644 --- a/internal/toproto6/datasourcemetadata_test.go +++ b/internal/toproto6/datasourcemetadata_test.go @@ -31,8 +31,6 @@ func TestDataSourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/diagnostics_test.go b/internal/toproto6/diagnostics_test.go index d473131b0..7e3ec0313 100644 --- a/internal/toproto6/diagnostics_test.go +++ b/internal/toproto6/diagnostics_test.go @@ -38,8 +38,6 @@ func TestDiagnosticSeverity(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -116,7 +114,6 @@ func TestDiagnostics(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/dynamic_value_test.go b/internal/toproto6/dynamic_value_test.go index efe2b8ee2..766f6b022 100644 --- a/internal/toproto6/dynamic_value_test.go +++ b/internal/toproto6/dynamic_value_test.go @@ -1230,8 +1230,6 @@ func TestDynamicValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/ephemeral_result_data_test.go b/internal/toproto6/ephemeral_result_data_test.go index 18cb2aced..14705f5fe 100644 --- a/internal/toproto6/ephemeral_result_data_test.go +++ b/internal/toproto6/ephemeral_result_data_test.go @@ -90,8 +90,6 @@ func TestEphemeralResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/ephemeralresourcemetadata_test.go b/internal/toproto6/ephemeralresourcemetadata_test.go index c62b90797..d14a22605 100644 --- a/internal/toproto6/ephemeralresourcemetadata_test.go +++ b/internal/toproto6/ephemeralresourcemetadata_test.go @@ -31,8 +31,6 @@ func TestEphemeralResourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/function_test.go b/internal/toproto6/function_test.go index f831cbe6a..793ab5202 100644 --- a/internal/toproto6/function_test.go +++ b/internal/toproto6/function_test.go @@ -215,8 +215,6 @@ func TestFunction(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +245,6 @@ func TestFunctionMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -456,8 +452,6 @@ func TestFunctionParameter(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -490,8 +484,6 @@ func TestFunctionReturn(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -530,8 +522,6 @@ func TestFunctionResultData(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/getfunctions_test.go b/internal/toproto6/getfunctions_test.go index 8de74f894..c3ebbe79a 100644 --- a/internal/toproto6/getfunctions_test.go +++ b/internal/toproto6/getfunctions_test.go @@ -221,8 +221,6 @@ func TestGetFunctionsResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go index 40a6b05e7..fe02ebf1c 100644 --- a/internal/toproto6/getmetadata_test.go +++ b/internal/toproto6/getmetadata_test.go @@ -173,8 +173,6 @@ func TestGetMetadataResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index 2df173187..1b3d1da19 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -2526,6 +2558,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4183,6 +4244,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Optional: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ @@ -5042,8 +5136,6 @@ func TestGetProviderSchemaResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/getresourceidentityschemas.go b/internal/toproto6/getresourceidentityschemas.go new file mode 100644 index 000000000..eb5d406e2 --- /dev/null +++ b/internal/toproto6/getresourceidentityschemas.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemasResponse returns the *tfprotov6.GetResourceIdentitySchemasResponse +// equivalent of a *fwserver.GetResourceIdentitySchemasResponse. +func GetResourceIdentitySchemasResponse(ctx context.Context, fw *fwserver.GetResourceIdentitySchemasResponse) *tfprotov6.GetResourceIdentitySchemasResponse { + if fw == nil { + return nil + } + + protov6 := &tfprotov6.GetResourceIdentitySchemasResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + IdentitySchemas: make(map[string]*tfprotov6.ResourceIdentitySchema, len(fw.IdentitySchemas)), + } + + var err error + + for resourceType, identitySchema := range fw.IdentitySchemas { + protov6.IdentitySchemas[resourceType], err = IdentitySchema(ctx, identitySchema) + + if err != nil { + protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error converting resource identity schema", + Detail: "The identity schema for the resource \"" + resourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov6 + } + } + + return protov6 +} diff --git a/internal/toproto6/getresourceidentityschemas_test.go b/internal/toproto6/getresourceidentityschemas_test.go new file mode 100644 index 000000000..86670bdf4 --- /dev/null +++ b/internal/toproto6/getresourceidentityschemas_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestGetResourceIdentitySchemasResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetResourceIdentitySchemasResponse + expected *tfprotov6.GetResourceIdentitySchemasResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "resource-identity-identity-multiple-resources": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource_1": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource_2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource_1": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + "test_resource_2": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-optionalforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + OptionalForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-requiredforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-bool": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-list-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-number": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.NumberAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + }, + "resource-identity-version": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Version: 123, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{}, + Version: 123, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.GetResourceIdentitySchemasResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/identity_schema.go b/internal/toproto6/identity_schema.go new file mode 100644 index 000000000..3c6455800 --- /dev/null +++ b/internal/toproto6/identity_schema.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema returns the *tfprotov6.ResourceIdentitySchema equivalent of a Schema. +func IdentitySchema(ctx context.Context, s fwschema.Schema) (*tfprotov6.ResourceIdentitySchema, error) { + if s == nil { + return nil, nil + } + + result := &tfprotov6.ResourceIdentitySchema{ + Version: s.GetVersion(), + } + + attrs := make([]*tfprotov6.ResourceIdentitySchemaAttribute, 0) + + for name, attr := range s.GetAttributes() { + a, err := IdentitySchemaAttribute(ctx, name, tftypes.NewAttributePath().WithAttributeName(name), attr) + + if err != nil { + return nil, err + } + + attrs = append(attrs, a) + } + + sort.Slice(attrs, func(i, j int) bool { + if attrs[i] == nil { + return true + } + + if attrs[j] == nil { + return false + } + + return attrs[i].Name < attrs[j].Name + }) + + result.IdentityAttributes = attrs + + return result, nil +} diff --git a/internal/toproto6/identity_schema_attribute.go b/internal/toproto6/identity_schema_attribute.go new file mode 100644 index 000000000..5fe815de1 --- /dev/null +++ b/internal/toproto6/identity_schema_attribute.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// IdentitySchemaAttribute returns the *tfprotov6.ResourceIdentitySchemaAttribute equivalent of an +// Attribute. Errors will be tftypes.AttributePathErrors based on `path`. `name` is the name of the attribute. +func IdentitySchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath, a fwschema.Attribute) (*tfprotov6.ResourceIdentitySchemaAttribute, error) { + if _, ok := a.(fwschema.NestedAttribute); ok { + return nil, path.NewErrorf("identity schemas don't support NestedAttribute") + } + + if a.GetType() == nil { + return nil, path.NewErrorf("must have Type set") + } + + if !a.IsRequiredForImport() && !a.IsOptionalForImport() { + return nil, path.NewErrorf("must have RequiredForImport or OptionalForImport set") + } + + identitySchemaAttribute := &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: name, + RequiredForImport: a.IsRequiredForImport(), + OptionalForImport: a.IsOptionalForImport(), + Type: a.GetType().TerraformType(ctx), + + // Unlike other schema attributes, identity attributes only have a single description field which + // is assumed to be markdown. Both a.GetDescription() and a.GetMarkdownDescription() will return + // the same string, so we just chose one here. + Description: a.GetDescription(), + } + + return identitySchemaAttribute, nil +} diff --git a/internal/toproto6/identity_schema_attribute_test.go b/internal/toproto6/identity_schema_attribute_test.go new file mode 100644 index 000000000..45fa0d1a1 --- /dev/null +++ b/internal/toproto6/identity_schema_attribute_test.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchemaAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr fwschema.Attribute + path *tftypes.AttributePath + expected *tfprotov6.ResourceIdentitySchemaAttribute + expectedErr string + } + + tests := map[string]testCase{ + "description": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + Description: "A string attribute", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + Description: "A string attribute", + }, + }, + "attr-string": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "attr-bool": { + name: "bool", + attr: testschema.Attribute{ + Type: types.BoolType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + "attr-number": { + name: "number", + attr: testschema.Attribute{ + Type: types.NumberType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "number", + Type: tftypes.Number, + RequiredForImport: true, + }, + }, + "attr-list": { + name: "list", + attr: testschema.Attribute{ + Type: types.ListType{ElemType: types.NumberType}, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "list", + Type: tftypes.List{ElementType: tftypes.Number}, + RequiredForImport: true, + }, + }, + "requiredforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "optionalforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + "nested-attr-single-error": { + name: "single_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-list-error": { + name: "list_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeList, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-map-error": { + name: "map_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeMap, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-set-error": { + name: "set_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSet, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "attr-unset": { + name: "whoops", + attr: testschema.Attribute{ + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have Type set", + }, + "missing-requiredforimport-and-optionalforimport": { + name: "whoops", + attr: testschema.Attribute{ + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have RequiredForImport or OptionalForImport set", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto6.IdentitySchemaAttribute(context.Background(), tc.name, tc.path, tc.attr) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto6/identity_schema_test.go b/internal/toproto6/identity_schema_test.go new file mode 100644 index 000000000..f8ebd1728 --- /dev/null +++ b/internal/toproto6/identity_schema_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input fwschema.Schema + expected *tfprotov6.ResourceIdentitySchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "empty-val": { + input: testschema.Schema{}, + expected: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{}, + Version: 0, + }, + }, + "basic-attrs": { + input: testschema.Schema{ + Version: 1, + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "number": testschema.Attribute{ + Type: types.NumberType, + OptionalForImport: true, + }, + "bool": testschema.Attribute{ + Type: types.BoolType, + OptionalForImport: true, + }, + }, + }, + expected: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + "complex-attrs": { + input: testschema.Schema{ + Version: 2, + Attributes: map[string]fwschema.Attribute{ + "list_of_string": testschema.Attribute{ + Type: types.ListType{ElemType: types.StringType}, + RequiredForImport: true, + }, + "list_of_bool": testschema.Attribute{ + Type: types.ListType{ElemType: types.BoolType}, + RequiredForImport: true, + }, + }, + }, + expected: &tfprotov6.ResourceIdentitySchema{ + Version: 2, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bool", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + { + Name: "list_of_string", + Type: tftypes.List{ElementType: tftypes.String}, + RequiredForImport: true, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto6.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto6/importedresource.go b/internal/toproto6/importedresource.go index 28fea4b58..a57ed6319 100644 --- a/internal/toproto6/importedresource.go +++ b/internal/toproto6/importedresource.go @@ -27,6 +27,11 @@ func ImportedResource(ctx context.Context, fw *fwserver.ImportedResource) (*tfpr proto6.State = state + identity, identityDiags := ResourceIdentity(ctx, fw.Identity) + + diags = append(diags, identityDiags...) + proto6.Identity = identity + newPrivate, privateDiags := fw.Private.Bytes(ctx) diags = append(diags, privateDiags...) diff --git a/internal/toproto6/importedresource_test.go b/internal/toproto6/importedresource_test.go index ce89d34f9..53eb11690 100644 --- a/internal/toproto6/importedresource_test.go +++ b/internal/toproto6/importedresource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -44,6 +45,22 @@ func TestImportResourceStateResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testEmptyProto6DynamicValue, err := tfprotov6.NewDynamicValue(testEmptyProto6Type, testEmptyProto6Value) if err != nil { @@ -85,6 +102,28 @@ func TestImportResourceStateResponse(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.ImportResourceStateResponse expected *tfprotov6.ImportResourceStateResponse @@ -154,6 +193,42 @@ func TestImportResourceStateResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-identity": { + input: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + Identity: testIdentityInvalid, + }, + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ImportResourceStateResponse{ ImportedResources: []fwserver.ImportedResource{ @@ -170,6 +245,26 @@ func TestImportResourceStateResponse(t *testing.T) { }, }, }, + "identity": { + input: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: testState, + Identity: testIdentity, + }, + }, + }, + expected: &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + State: &testProto6DynamicValue, + Identity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, + }, + }, "private": { input: &fwserver.ImportResourceStateResponse{ ImportedResources: []fwserver.ImportedResource{ @@ -199,8 +294,6 @@ func TestImportResourceStateResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/moveresourcestate.go b/internal/toproto6/moveresourcestate.go index 86c2a55a8..83a81d446 100644 --- a/internal/toproto6/moveresourcestate.go +++ b/internal/toproto6/moveresourcestate.go @@ -27,6 +27,11 @@ func MoveResourceStateResponse(ctx context.Context, fw *fwserver.MoveResourceSta proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.TargetPrivate = targetPrivate + targetIdentity, diags := ResourceIdentity(ctx, fw.TargetIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.TargetIdentity = targetIdentity + targetState, diags := State(ctx, fw.TargetState) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/moveresourcestate_test.go b/internal/toproto6/moveresourcestate_test.go index 8a40223ee..dfed88c69 100644 --- a/internal/toproto6/moveresourcestate_test.go +++ b/internal/toproto6/moveresourcestate_test.go @@ -5,10 +5,11 @@ package toproto6_test import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -33,6 +34,44 @@ func TestMoveResourceStateResponse(t *testing.T) { }, ) + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.MoveResourceStateResponse expected *tfprotov6.MoveResourceStateResponse @@ -152,11 +191,50 @@ func TestMoveResourceStateResponse(t *testing.T) { }, }, }, + "TargetIdentity": { + input: &fwserver.MoveResourceStateResponse{ + TargetIdentity: testIdentity, + }, + expected: &tfprotov6.MoveResourceStateResponse{ + TargetIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, + "TargetIdentity-invalid": { + input: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + TargetIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/openephemeralresource_test.go b/internal/toproto6/openephemeralresource_test.go index 734294018..4343c3e26 100644 --- a/internal/toproto6/openephemeralresource_test.go +++ b/internal/toproto6/openephemeralresource_test.go @@ -199,8 +199,6 @@ func TestOpenEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index 486f7ab02..cf9b2642a 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -29,6 +29,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.PlannedState = plannedState + plannedIdentity, diags := ResourceIdentity(ctx, fw.PlannedIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.PlannedIdentity = plannedIdentity + requiresReplace, diags := totftypes.AttributePaths(ctx, fw.RequiresReplace) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index ad8376e16..344ac0a21 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -40,6 +41,22 @@ func TestPlanResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -62,6 +79,28 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -143,6 +182,37 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + PlannedIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "plannedprivate-empty": { input: &fwserver.PlanResourceChangeResponse{ PlannedPrivate: &privatestate.Data{ @@ -177,6 +247,16 @@ func TestPlanResourceChangeResponse(t *testing.T) { PlannedState: &testProto6DynamicValue, }, }, + "plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: testIdentity, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "requiresreplace": { input: &fwserver.PlanResourceChangeResponse{ RequiresReplace: path.Paths{ @@ -200,8 +280,6 @@ func TestPlanResourceChangeResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/readdatasource_test.go b/internal/toproto6/readdatasource_test.go index 593452609..062e0e0ba 100644 --- a/internal/toproto6/readdatasource_test.go +++ b/internal/toproto6/readdatasource_test.go @@ -152,8 +152,6 @@ func TestReadDataSourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index 2de7adf84..ea60e5a0f 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -28,6 +28,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index 46d928c31..0596dc30c 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -39,6 +40,22 @@ func TestReadResourceResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -61,6 +78,28 @@ func TestReadResourceResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -142,6 +181,37 @@ func TestReadResourceResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.ReadResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ReadResourceResponse{ NewState: testState, @@ -150,6 +220,16 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto6DynamicValue, }, }, + "newidentity": { + input: &fwserver.ReadResourceResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov6.ReadResourceResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "private-empty": { input: &fwserver.ReadResourceResponse{ Private: &privatestate.Data{ @@ -187,8 +267,6 @@ func TestReadResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/renewephemeralresource_test.go b/internal/toproto6/renewephemeralresource_test.go index 54c3b4f64..1491f3bc4 100644 --- a/internal/toproto6/renewephemeralresource_test.go +++ b/internal/toproto6/renewephemeralresource_test.go @@ -102,8 +102,6 @@ func TestRenewEphemeralResourceResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/resource_identity.go b/internal/toproto6/resource_identity.go new file mode 100644 index 000000000..3dac3de99 --- /dev/null +++ b/internal/toproto6/resource_identity.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ResourceIdentity returns the *tfprotov6.ResourceIdentityData for a *tfsdk.ResourceIdentity. +func ResourceIdentity(ctx context.Context, fw *tfsdk.ResourceIdentity) (*tfprotov6.ResourceIdentityData, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + identitySchemaData := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + identityData, diags := DynamicValue(ctx, identitySchemaData) + if diags.HasError() { + return nil, diags + } + + return &tfprotov6.ResourceIdentityData{ + IdentityData: identityData, + }, nil +} diff --git a/internal/toproto6/resource_identity_test.go b/internal/toproto6/resource_identity_test.go new file mode 100644 index 000000000..699e29b64 --- /dev/null +++ b/internal/toproto6/resource_identity_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testResourceIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.ResourceIdentity + expected *tfprotov6.ResourceIdentityData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testResourceIdentityInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testResourceIdentity, + expected: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto6.ResourceIdentity(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/resourcemetadata_test.go b/internal/toproto6/resourcemetadata_test.go index 17f9463e1..d4e561e72 100644 --- a/internal/toproto6/resourcemetadata_test.go +++ b/internal/toproto6/resourcemetadata_test.go @@ -31,8 +31,6 @@ func TestResourceMetadata(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/schema_attribute.go b/internal/toproto6/schema_attribute.go index 492a5a2ab..d020a95d2 100644 --- a/internal/toproto6/schema_attribute.go +++ b/internal/toproto6/schema_attribute.go @@ -7,9 +7,10 @@ import ( "context" "sort" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov6.SchemaAttribute equivalent of an @@ -27,6 +28,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { diff --git a/internal/toproto6/schema_attribute_test.go b/internal/toproto6/schema_attribute_test.go index 595694bb9..1eb83c0c0 100644 --- a/internal/toproto6/schema_attribute_test.go +++ b/internal/toproto6/schema_attribute_test.go @@ -437,7 +437,6 @@ func TestSchemaAttribute(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/schema_test.go b/internal/toproto6/schema_test.go index e3888b639..50d203c61 100644 --- a/internal/toproto6/schema_test.go +++ b/internal/toproto6/schema_test.go @@ -627,7 +627,6 @@ func TestSchema(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/server_capabilities_test.go b/internal/toproto6/server_capabilities_test.go index a9e6955fe..557db6c32 100644 --- a/internal/toproto6/server_capabilities_test.go +++ b/internal/toproto6/server_capabilities_test.go @@ -51,8 +51,6 @@ func TestServerCapabilities(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/state_test.go b/internal/toproto6/state_test.go index 70f36ce08..0316e3524 100644 --- a/internal/toproto6/state_test.go +++ b/internal/toproto6/state_test.go @@ -90,8 +90,6 @@ func TestState(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/upgraderesourcestate_test.go b/internal/toproto6/upgraderesourcestate_test.go index 70928db44..d31589d55 100644 --- a/internal/toproto6/upgraderesourcestate_test.go +++ b/internal/toproto6/upgraderesourcestate_test.go @@ -134,8 +134,6 @@ func TestUpgradeResourceStateResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/validatedatasourceconfig_test.go b/internal/toproto6/validatedatasourceconfig_test.go index 879b6982d..460807eb6 100644 --- a/internal/toproto6/validatedatasourceconfig_test.go +++ b/internal/toproto6/validatedatasourceconfig_test.go @@ -54,8 +54,6 @@ func TestValidateDataSourceConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/validateephemeralresourceconfig_test.go b/internal/toproto6/validateephemeralresourceconfig_test.go index a088f2b88..3b41fdf1d 100644 --- a/internal/toproto6/validateephemeralresourceconfig_test.go +++ b/internal/toproto6/validateephemeralresourceconfig_test.go @@ -54,8 +54,6 @@ func TestValidateEphemeralResourceConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/validateproviderconfig_test.go b/internal/toproto6/validateproviderconfig_test.go index 8f8c2ba90..ffe9adb93 100644 --- a/internal/toproto6/validateproviderconfig_test.go +++ b/internal/toproto6/validateproviderconfig_test.go @@ -134,8 +134,6 @@ func TestValidateProviderConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/toproto6/validateresourceconfig_test.go b/internal/toproto6/validateresourceconfig_test.go index 783487a97..c673ad911 100644 --- a/internal/toproto6/validateresourceconfig_test.go +++ b/internal/toproto6/validateresourceconfig_test.go @@ -54,8 +54,6 @@ func TestValidateResourceConfigResponse(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/totftypes/attribute_path_step_test.go b/internal/totftypes/attribute_path_step_test.go index f5070953b..796ec9b94 100644 --- a/internal/totftypes/attribute_path_step_test.go +++ b/internal/totftypes/attribute_path_step_test.go @@ -48,8 +48,6 @@ func TestAttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/totftypes/attribute_path_test.go b/internal/totftypes/attribute_path_test.go index eb618ca34..f0b957c1d 100644 --- a/internal/totftypes/attribute_path_test.go +++ b/internal/totftypes/attribute_path_test.go @@ -52,8 +52,6 @@ func TestAttributePath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/totftypes/attribute_paths_test.go b/internal/totftypes/attribute_paths_test.go index b8daf3c06..d23901425 100644 --- a/internal/totftypes/attribute_paths_test.go +++ b/internal/totftypes/attribute_paths_test.go @@ -87,8 +87,6 @@ func TestAttributePaths(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_attribute_name_exact_test.go b/path/expression_step_attribute_name_exact_test.go index 46ec2f278..379f75e5b 100644 --- a/path/expression_step_attribute_name_exact_test.go +++ b/path/expression_step_attribute_name_exact_test.go @@ -47,8 +47,6 @@ func TestExpressionStepAttributeNameExactEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -97,8 +95,6 @@ func TestExpressionStepAttributeNameExactMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -125,8 +121,6 @@ func TestExpressionStepAttributeNameExactString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_int_any_test.go b/path/expression_step_element_key_int_any_test.go index 3308ad4b9..a8752d6ea 100644 --- a/path/expression_step_element_key_int_any_test.go +++ b/path/expression_step_element_key_int_any_test.go @@ -47,8 +47,6 @@ func TestExpressionStepElementKeyIntAnyEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -92,8 +90,6 @@ func TestExpressionStepElementKeyIntAnyMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -120,8 +116,6 @@ func TestExpressionStepElementKeyIntAnyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_int_exact_test.go b/path/expression_step_element_key_int_exact_test.go index 7ae70ce20..f9c24c740 100644 --- a/path/expression_step_element_key_int_exact_test.go +++ b/path/expression_step_element_key_int_exact_test.go @@ -52,8 +52,6 @@ func TestExpressionStepElementKeyIntExactEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -102,8 +100,6 @@ func TestExpressionStepElementKeyIntExactMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -130,8 +126,6 @@ func TestExpressionStepElementKeyIntExactString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_string_any_test.go b/path/expression_step_element_key_string_any_test.go index 7af02ae70..c7fa43f1f 100644 --- a/path/expression_step_element_key_string_any_test.go +++ b/path/expression_step_element_key_string_any_test.go @@ -47,8 +47,6 @@ func TestExpressionStepElementKeyStringAnyEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -92,8 +90,6 @@ func TestExpressionStepElementKeyStringAnyMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -120,8 +116,6 @@ func TestExpressionStepElementKeyStringAnyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_string_exact_test.go b/path/expression_step_element_key_string_exact_test.go index 16a935045..ec2da5920 100644 --- a/path/expression_step_element_key_string_exact_test.go +++ b/path/expression_step_element_key_string_exact_test.go @@ -47,8 +47,6 @@ func TestExpressionStepElementKeyStringExactEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -97,8 +95,6 @@ func TestExpressionStepElementKeyStringExactMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -129,8 +125,6 @@ func TestExpressionStepElementKeyStringExactString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_value_any_test.go b/path/expression_step_element_key_value_any_test.go index 78ee79e56..ae4ef52fe 100644 --- a/path/expression_step_element_key_value_any_test.go +++ b/path/expression_step_element_key_value_any_test.go @@ -47,8 +47,6 @@ func TestExpressionStepElementKeyValueAnyEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -92,8 +90,6 @@ func TestExpressionStepElementKeyValueAnyMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -120,8 +116,6 @@ func TestExpressionStepElementKeyValueAnyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_element_key_value_exact_test.go b/path/expression_step_element_key_value_exact_test.go index 136965c4f..c12fa0bd6 100644 --- a/path/expression_step_element_key_value_exact_test.go +++ b/path/expression_step_element_key_value_exact_test.go @@ -48,8 +48,6 @@ func TestExpressionStepElementKeyValueExactEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -98,8 +96,6 @@ func TestExpressionStepElementKeyValueExactMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +175,6 @@ func TestExpressionStepElementKeyValueExactString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_step_parent_test.go b/path/expression_step_parent_test.go index 044f55ee2..496a98e09 100644 --- a/path/expression_step_parent_test.go +++ b/path/expression_step_parent_test.go @@ -47,8 +47,6 @@ func TestExpressionStepParentEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -92,8 +90,6 @@ func TestExpressionStepParentMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -120,8 +116,6 @@ func TestExpressionStepParentString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_steps_test.go b/path/expression_steps_test.go index 0e5fa3e97..02cb01d80 100644 --- a/path/expression_steps_test.go +++ b/path/expression_steps_test.go @@ -54,8 +54,6 @@ func TestExpressionStepsAppend(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestExpressionStepsCopy(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +296,6 @@ func TestExpressionStepsEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,8 +358,6 @@ func TestExpressionStepsLastStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -652,8 +644,6 @@ func TestExpressionStepsMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1068,8 +1058,6 @@ func TestExpressionStepsMatchesParent(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1132,8 +1120,6 @@ func TestExpressionStepsNextStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1253,8 +1239,6 @@ func TestExpressionStepsResolve(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1394,8 +1378,6 @@ func TestExpressionStepsString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expression_test.go b/path/expression_test.go index 982714a65..70664f5a1 100644 --- a/path/expression_test.go +++ b/path/expression_test.go @@ -30,8 +30,6 @@ func TestExpressionAtAnyListIndex(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -62,8 +60,6 @@ func TestExpressionAtAnyMapKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -94,8 +90,6 @@ func TestExpressionAtAnySetValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -129,8 +123,6 @@ func TestExpressionAtListIndex(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +156,6 @@ func TestExpressionAtMapKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +189,6 @@ func TestExpressionAtName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +219,6 @@ func TestExpressionAtParent(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -266,8 +252,6 @@ func TestExpressionAtSetValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -298,8 +282,6 @@ func TestExpressionCopy(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -348,8 +330,6 @@ func TestExpressionEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -493,8 +473,6 @@ func TestExpressionMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -698,8 +676,6 @@ func TestExpressionMatchesParent(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -743,8 +719,6 @@ func TestExpressionMerge(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -829,8 +803,6 @@ func TestExpressionMergeExpressions(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -871,8 +843,6 @@ func TestExpressionResolve(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -922,8 +892,6 @@ func TestExpressionSteps(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1011,8 +979,6 @@ func TestExpressionString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/expressions_test.go b/path/expressions_test.go index 6aca104f4..92b3060f2 100644 --- a/path/expressions_test.go +++ b/path/expressions_test.go @@ -83,8 +83,6 @@ func TestExpressionsAppend(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -210,8 +208,6 @@ func TestExpressionsContains(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -347,8 +343,6 @@ func TestExpressionsMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -405,8 +399,6 @@ func TestExpressionsString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_step_attribute_name_test.go b/path/path_step_attribute_name_test.go index 5e5a0c215..780c7ee85 100644 --- a/path/path_step_attribute_name_test.go +++ b/path/path_step_attribute_name_test.go @@ -47,8 +47,6 @@ func TestPathStepAttributeNameEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -75,8 +73,6 @@ func TestPathStepAttributeNameExpressionStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +99,6 @@ func TestPathStepAttributeNameString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_int_test.go b/path/path_step_element_key_int_test.go index 1599cd263..ec2601714 100644 --- a/path/path_step_element_key_int_test.go +++ b/path/path_step_element_key_int_test.go @@ -47,8 +47,6 @@ func TestPathStepElementKeyIntEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -75,8 +73,6 @@ func TestPathStepElementKeyIntExpressionStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +99,6 @@ func TestPathStepElementKeyIntString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_string_test.go b/path/path_step_element_key_string_test.go index 222394e5a..4a80a038e 100644 --- a/path/path_step_element_key_string_test.go +++ b/path/path_step_element_key_string_test.go @@ -47,8 +47,6 @@ func TestPathStepElementKeyStringEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -75,8 +73,6 @@ func TestPathStepElementKeyStringExpressionStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -107,8 +103,6 @@ func TestPathStepElementKeyStringString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_step_element_key_value_test.go b/path/path_step_element_key_value_test.go index 4039df817..d7e8af659 100644 --- a/path/path_step_element_key_value_test.go +++ b/path/path_step_element_key_value_test.go @@ -53,8 +53,6 @@ func TestPathStepElementKeyValueEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -81,8 +79,6 @@ func TestPathStepElementKeyValueExpressionStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -162,8 +158,6 @@ func TestPathStepElementKeyValueString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_steps_test.go b/path/path_steps_test.go index 3a46e7fb9..2e3f1e604 100644 --- a/path/path_steps_test.go +++ b/path/path_steps_test.go @@ -54,8 +54,6 @@ func TestPathStepsAppend(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestPathStepsCopy(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +296,6 @@ func TestPathStepsEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +344,6 @@ func TestPathStepsExpressionSteps(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -414,8 +406,6 @@ func TestPathStepsLastStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -482,8 +472,6 @@ func TestPathStepsNextStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -627,8 +615,6 @@ func TestPathStepsString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/path_test.go b/path/path_test.go index 7742e4335..80155d06a 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -38,8 +38,6 @@ func TestPathAtListIndex(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -78,8 +76,6 @@ func TestPathAtTupleIndex(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -118,8 +114,6 @@ func TestPathAtMapKey(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -158,8 +152,6 @@ func TestPathAtName(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -198,8 +190,6 @@ func TestPathAtSetValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -234,8 +224,6 @@ func TestPathCopy(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -289,8 +277,6 @@ func TestPathEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -321,8 +307,6 @@ func TestPathExpression(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +341,6 @@ func TestPathParentPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,8 +380,6 @@ func TestPathSteps(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -491,8 +471,6 @@ func TestPathString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/path/paths_test.go b/path/paths_test.go index 78a9d3561..a7ff2a422 100644 --- a/path/paths_test.go +++ b/path/paths_test.go @@ -83,8 +83,6 @@ func TestPathsAppend(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -203,8 +201,6 @@ func TestPathsContains(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +257,6 @@ func TestPathsString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 6d96a516a..d02fcc801 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,21 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go index 617225b54..caeefaeb1 100644 --- a/provider/metaschema/bool_attribute_test.go +++ b/provider/metaschema/bool_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -54,8 +55,6 @@ func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,8 +95,6 @@ func TestBoolAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -131,8 +128,6 @@ func TestBoolAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +160,6 @@ func TestBoolAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +192,6 @@ func TestBoolAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -233,8 +224,6 @@ func TestBoolAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +250,6 @@ func TestBoolAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +282,6 @@ func TestBoolAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +314,6 @@ func TestBoolAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +340,6 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,3 +351,81 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index 8a4478655..96c634fbf 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,21 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go index 71cb2f688..0ee1b3d42 100644 --- a/provider/metaschema/float64_attribute_test.go +++ b/provider/metaschema/float64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -54,8 +55,6 @@ func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,8 +95,6 @@ func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -131,8 +128,6 @@ func TestFloat64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +160,6 @@ func TestFloat64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +192,6 @@ func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -233,8 +224,6 @@ func TestFloat64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +250,6 @@ func TestFloat64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +282,6 @@ func TestFloat64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +314,6 @@ func TestFloat64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +340,6 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,3 +351,81 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index 8751d574e..7490bd7b5 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,21 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go index 28efcebd7..ab7a5bdf6 100644 --- a/provider/metaschema/int64_attribute_test.go +++ b/provider/metaschema/int64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -54,8 +55,6 @@ func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,8 +95,6 @@ func TestInt64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -131,8 +128,6 @@ func TestInt64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +160,6 @@ func TestInt64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +192,6 @@ func TestInt64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -233,8 +224,6 @@ func TestInt64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +250,6 @@ func TestInt64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +282,6 @@ func TestInt64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +314,6 @@ func TestInt64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +340,6 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,3 +351,81 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index a3ff30e65..ea663a8a9 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -134,6 +135,24 @@ func (a ListAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/list_attribute_test.go b/provider/metaschema/list_attribute_test.go index 6c864e8d6..66f5e6b66 100644 --- a/provider/metaschema/list_attribute_test.go +++ b/provider/metaschema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -57,8 +58,6 @@ func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -99,8 +98,6 @@ func TestListAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +136,6 @@ func TestListAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +168,6 @@ func TestListAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +200,6 @@ func TestListAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +232,6 @@ func TestListAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,8 +258,6 @@ func TestListAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -303,8 +290,6 @@ func TestListAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -337,8 +322,6 @@ func TestListAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -365,8 +348,6 @@ func TestListAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,6 +360,32 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -432,8 +439,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -446,3 +451,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index a6b4f875a..ccc3293d6 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,21 @@ func (a ListNestedAttribute) IsRequired() bool { func (a ListNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go index dc6188822..fe587af0d 100644 --- a/provider/metaschema/list_nested_attribute_test.go +++ b/provider/metaschema/list_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -82,8 +83,6 @@ func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -130,8 +129,6 @@ func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -221,8 +218,6 @@ func TestListNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +256,6 @@ func TestListNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +294,6 @@ func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -339,8 +330,6 @@ func TestListNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -385,8 +374,6 @@ func TestListNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -419,8 +406,6 @@ func TestListNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +444,6 @@ func TestListNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -499,8 +482,6 @@ func TestListNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -533,8 +514,6 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -546,3 +525,81 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index 51ee02edb..99d90e767 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -137,6 +138,24 @@ func (a MapAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/map_attribute_test.go b/provider/metaschema/map_attribute_test.go index 0956d8cc4..11ec28d01 100644 --- a/provider/metaschema/map_attribute_test.go +++ b/provider/metaschema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -57,8 +58,6 @@ func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -99,8 +98,6 @@ func TestMapAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +136,6 @@ func TestMapAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +168,6 @@ func TestMapAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +200,6 @@ func TestMapAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +232,6 @@ func TestMapAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,8 +258,6 @@ func TestMapAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -303,8 +290,6 @@ func TestMapAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -337,8 +322,6 @@ func TestMapAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -365,8 +348,6 @@ func TestMapAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,6 +360,32 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -432,8 +439,6 @@ func TestMapAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -446,3 +451,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 47a3b4348..79029c833 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,21 @@ func (a MapNestedAttribute) IsRequired() bool { func (a MapNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go index 7c320b1bf..b2cc9a219 100644 --- a/provider/metaschema/map_nested_attribute_test.go +++ b/provider/metaschema/map_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -82,8 +83,6 @@ func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -130,8 +129,6 @@ func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -221,8 +218,6 @@ func TestMapNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +256,6 @@ func TestMapNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +294,6 @@ func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -339,8 +330,6 @@ func TestMapNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -385,8 +374,6 @@ func TestMapNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -419,8 +406,6 @@ func TestMapNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +444,6 @@ func TestMapNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -499,8 +482,6 @@ func TestMapNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -533,8 +514,6 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -546,3 +525,81 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/nested_attribute_object_test.go b/provider/metaschema/nested_attribute_object_test.go index c9ca5a210..3ea0c0ac1 100644 --- a/provider/metaschema/nested_attribute_object_test.go +++ b/provider/metaschema/nested_attribute_object_test.go @@ -79,8 +79,6 @@ func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -144,8 +142,6 @@ func TestNestedAttributeObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -184,8 +180,6 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -226,8 +220,6 @@ func TestNestedAttributeObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 86e45b8ab..3e345a7fb 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -121,3 +122,21 @@ func (a NumberAttribute) IsRequired() bool { func (a NumberAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go index 819f2d2a1..53a094126 100644 --- a/provider/metaschema/number_attribute_test.go +++ b/provider/metaschema/number_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -54,8 +55,6 @@ func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,8 +95,6 @@ func TestNumberAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -131,8 +128,6 @@ func TestNumberAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +160,6 @@ func TestNumberAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +192,6 @@ func TestNumberAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -233,8 +224,6 @@ func TestNumberAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +250,6 @@ func TestNumberAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +282,6 @@ func TestNumberAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +314,6 @@ func TestNumberAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +340,6 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,3 +351,81 @@ func TestNumberAttributeIsSensitive(t *testing.T) { }) } } + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index aa4c67be9..7c325a238 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -136,6 +137,24 @@ func (a ObjectAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/object_attribute_test.go b/provider/metaschema/object_attribute_test.go index e24db5dd1..7b47aa5c9 100644 --- a/provider/metaschema/object_attribute_test.go +++ b/provider/metaschema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -63,8 +64,6 @@ func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -105,8 +104,6 @@ func TestObjectAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +142,6 @@ func TestObjectAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +174,6 @@ func TestObjectAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,8 +206,6 @@ func TestObjectAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +238,6 @@ func TestObjectAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +264,6 @@ func TestObjectAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -309,8 +296,6 @@ func TestObjectAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,8 +328,6 @@ func TestObjectAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -371,8 +354,6 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -385,6 +366,32 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -440,8 +447,6 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -454,3 +459,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/schema_test.go b/provider/metaschema/schema_test.go index f4b64875f..7460f8016 100644 --- a/provider/metaschema/schema_test.go +++ b/provider/metaschema/schema_test.go @@ -82,8 +82,6 @@ func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -223,7 +221,6 @@ func TestSchemaAttributeAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -325,7 +322,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -383,8 +379,6 @@ func TestSchemaGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -411,8 +405,6 @@ func TestSchemaGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -443,8 +435,6 @@ func TestSchemaGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -475,8 +465,6 @@ func TestSchemaGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -507,8 +495,6 @@ func TestSchemaGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -539,8 +525,6 @@ func TestSchemaGetVersion(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -575,8 +559,6 @@ func TestSchemaType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -683,8 +665,6 @@ func TestSchemaTypeAtPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -783,8 +763,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -840,8 +818,6 @@ func TestSchemaValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -945,8 +921,6 @@ func TestSchemaValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index 919713075..5878c6ec8 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -132,6 +133,24 @@ func (a SetAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/set_attribute_test.go b/provider/metaschema/set_attribute_test.go index 8cb5a95d5..94bf6346b 100644 --- a/provider/metaschema/set_attribute_test.go +++ b/provider/metaschema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -57,8 +58,6 @@ func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -99,8 +98,6 @@ func TestSetAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +136,6 @@ func TestSetAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +168,6 @@ func TestSetAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +200,6 @@ func TestSetAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +232,6 @@ func TestSetAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,8 +258,6 @@ func TestSetAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -303,8 +290,6 @@ func TestSetAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -337,8 +322,6 @@ func TestSetAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -365,8 +348,6 @@ func TestSetAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,6 +360,32 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -432,8 +439,6 @@ func TestSetAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -446,3 +451,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index 233866a00..102ede616 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -154,3 +155,21 @@ func (a SetNestedAttribute) IsRequired() bool { func (a SetNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go index 623fa3668..5e7c46f66 100644 --- a/provider/metaschema/set_nested_attribute_test.go +++ b/provider/metaschema/set_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -82,8 +83,6 @@ func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -130,8 +129,6 @@ func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -221,8 +218,6 @@ func TestSetNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +256,6 @@ func TestSetNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +294,6 @@ func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -339,8 +330,6 @@ func TestSetNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -385,8 +374,6 @@ func TestSetNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -419,8 +406,6 @@ func TestSetNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +444,6 @@ func TestSetNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -499,8 +482,6 @@ func TestSetNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -533,8 +514,6 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -546,3 +525,81 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index 0ed1a22fd..51b7c73b6 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,3 +175,21 @@ func (a SingleNestedAttribute) IsRequired() bool { func (a SingleNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go index f7c7cbda5..82b57349f 100644 --- a/provider/metaschema/single_nested_attribute_test.go +++ b/provider/metaschema/single_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -80,8 +81,6 @@ func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -171,8 +170,6 @@ func TestSingleNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -203,8 +200,6 @@ func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +236,6 @@ func TestSingleNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -279,8 +272,6 @@ func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -315,8 +306,6 @@ func TestSingleNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +346,6 @@ func TestSingleNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -389,8 +376,6 @@ func TestSingleNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -427,8 +412,6 @@ func TestSingleNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -465,8 +448,6 @@ func TestSingleNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -497,8 +478,6 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -510,3 +489,81 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 3a14d0721..eb056e2d1 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,21 @@ func (a StringAttribute) IsRequired() bool { func (a StringAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go index 544fa268a..2d12a2f58 100644 --- a/provider/metaschema/string_attribute_test.go +++ b/provider/metaschema/string_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -54,8 +55,6 @@ func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,8 +95,6 @@ func TestStringAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -131,8 +128,6 @@ func TestStringAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -165,8 +160,6 @@ func TestStringAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -199,8 +192,6 @@ func TestStringAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -233,8 +224,6 @@ func TestStringAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +250,6 @@ func TestStringAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -295,8 +282,6 @@ func TestStringAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +314,6 @@ func TestStringAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,8 +340,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,3 +351,81 @@ func TestStringAttributeIsSensitive(t *testing.T) { }) } } + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/provider.go b/provider/provider.go index ba18927cb..61dfd39fe 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -90,9 +90,6 @@ type ProviderWithFunctions interface { // include ephemeral resources for usage in practitioner configurations. // // Ephemeral resources are supported in Terraform version 1.10 and later. -// -// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until -// these notices are removed. type ProviderWithEphemeralResources interface { Provider diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index c411062e0..9f4f37138 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,3 +179,21 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/bool_attribute_test.go b/provider/schema/bool_attribute_test.go index 043f2a896..07291287b 100644 --- a/provider/schema/bool_attribute_test.go +++ b/provider/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestBoolAttributeBoolValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestBoolAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestBoolAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestBoolAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestBoolAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestBoolAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestBoolAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestBoolAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestBoolAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -404,8 +385,6 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +396,81 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index c738d348d..86da9aef2 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -175,3 +176,21 @@ func (a DynamicAttribute) IsSensitive() bool { func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/dynamic_attribute_test.go b/provider/schema/dynamic_attribute_test.go index c06eaf811..bd6ec94a7 100644 --- a/provider/schema/dynamic_attribute_test.go +++ b/provider/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestDynamicAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestDynamicAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestDynamicAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -268,8 +257,6 @@ func TestDynamicAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestDynamicAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestDynamicAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,6 +365,32 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() @@ -404,8 +411,6 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +422,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index a36c5c435..bd124a33f 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,21 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go index e779b4a0a..2793464d7 100644 --- a/provider/schema/float32_attribute_test.go +++ b/provider/schema/float32_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestFloat32AttributeFloat32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestFloat32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestFloat32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestFloat32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestFloat32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestFloat32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestFloat32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -404,8 +385,6 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +396,81 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index 786965e3a..d7c328329 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,21 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/float64_attribute_test.go b/provider/schema/float64_attribute_test.go index 96604d0e7..01db35d43 100644 --- a/provider/schema/float64_attribute_test.go +++ b/provider/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestFloat64AttributeFloat64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -137,8 +134,6 @@ func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestFloat64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestFloat64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestFloat64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestFloat64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestFloat64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestFloat64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -404,8 +385,6 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +396,81 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 16ff58f0f..1c5bde95f 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -182,3 +182,21 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go index b3ac0687c..f93d28ef0 100644 --- a/provider/schema/int32_attribute_test.go +++ b/provider/schema/int32_attribute_test.go @@ -56,8 +56,6 @@ func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -104,8 +102,6 @@ func TestInt32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -139,8 +135,6 @@ func TestInt32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -173,8 +167,6 @@ func TestInt32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -207,8 +199,6 @@ func TestInt32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -241,8 +231,6 @@ func TestInt32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -275,8 +263,6 @@ func TestInt32AttributeInt32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -303,8 +289,6 @@ func TestInt32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -337,8 +321,6 @@ func TestInt32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -371,8 +353,6 @@ func TestInt32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -405,8 +385,6 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,3 +396,81 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 3fd9713f1..bf334a6d3 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,21 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/int64_attribute_test.go b/provider/schema/int64_attribute_test.go index f5e457663..c2bdc7d1d 100644 --- a/provider/schema/int64_attribute_test.go +++ b/provider/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestInt64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestInt64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestInt64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestInt64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestInt64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,8 +263,6 @@ func TestInt64AttributeInt64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestInt64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestInt64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestInt64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -404,8 +385,6 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +396,81 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index e733b297f..55f6f233c 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -195,6 +196,24 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_attribute_test.go b/provider/schema/list_attribute_test.go index 3baa9382b..bb8451841 100644 --- a/provider/schema/list_attribute_test.go +++ b/provider/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestListAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestListAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestListAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestListAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestListAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +265,6 @@ func TestListAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -310,8 +297,6 @@ func TestListAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -344,8 +329,6 @@ func TestListAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +361,6 @@ func TestListAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -392,6 +373,32 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() @@ -412,8 +419,6 @@ func TestListAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -501,8 +506,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,3 +518,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 0c82da4ad..e00e940b0 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -223,6 +224,24 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_nested_attribute_test.go b/provider/schema/list_nested_attribute_test.go index 43d76a612..b6130073e 100644 --- a/provider/schema/list_nested_attribute_test.go +++ b/provider/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestListNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestListNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestListNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestListNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -429,8 +416,6 @@ func TestListNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -469,8 +454,6 @@ func TestListNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -509,8 +492,6 @@ func TestListNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -549,8 +530,6 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -563,6 +542,32 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() @@ -589,8 +594,6 @@ func TestListNestedAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -668,8 +671,6 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -682,3 +683,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_nested_block_test.go b/provider/schema/list_nested_block_test.go index f7042840e..a5891a4f5 100644 --- a/provider/schema/list_nested_block_test.go +++ b/provider/schema/list_nested_block_test.go @@ -86,8 +86,6 @@ func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestListNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestListNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestListNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestListNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestListNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestListNestedBlockListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestListNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestListNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index 77dc2b61d..28cc5f5b8 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,6 +199,24 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_attribute_test.go b/provider/schema/map_attribute_test.go index 0bbef5f25..bf46b6c5a 100644 --- a/provider/schema/map_attribute_test.go +++ b/provider/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestMapAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestMapAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestMapAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestMapAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestMapAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +265,6 @@ func TestMapAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -310,8 +297,6 @@ func TestMapAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -344,8 +329,6 @@ func TestMapAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +361,6 @@ func TestMapAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -392,6 +373,32 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() @@ -412,8 +419,6 @@ func TestMapAttributeMapValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -501,8 +506,6 @@ func TestMapAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,3 +518,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 2eed2fa08..5b8246491 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -223,6 +223,24 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_nested_attribute_test.go b/provider/schema/map_nested_attribute_test.go index 193960869..26812fc03 100644 --- a/provider/schema/map_nested_attribute_test.go +++ b/provider/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestMapNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestMapNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestMapNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestMapNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -429,8 +416,6 @@ func TestMapNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -469,8 +454,6 @@ func TestMapNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -509,8 +492,6 @@ func TestMapNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -549,8 +530,6 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -563,6 +542,32 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() @@ -589,8 +594,6 @@ func TestMapNestedAttributeMapNestedValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -668,8 +671,6 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -682,3 +683,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/nested_attribute_object_test.go b/provider/schema/nested_attribute_object_test.go index 968d071d0..6a14e6344 100644 --- a/provider/schema/nested_attribute_object_test.go +++ b/provider/schema/nested_attribute_object_test.go @@ -80,8 +80,6 @@ func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestNestedAttributeObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +181,6 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -223,8 +217,6 @@ func TestNestedAttributeObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -265,8 +257,6 @@ func TestNestedAttributeObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/nested_block_object_test.go b/provider/schema/nested_block_object_test.go index 5fa2a9c7e..3dd2beaf3 100644 --- a/provider/schema/nested_block_object_test.go +++ b/provider/schema/nested_block_object_test.go @@ -98,8 +98,6 @@ func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -163,8 +161,6 @@ func TestNestedBlockObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -203,8 +199,6 @@ func TestNestedBlockObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -259,8 +253,6 @@ func TestNestedBlockObjectGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -297,8 +289,6 @@ func TestNestedBlockObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -351,8 +341,6 @@ func TestNestedBlockObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index f3e90e2b7..add021b1c 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,6 +179,24 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/provider/schema/number_attribute_test.go b/provider/schema/number_attribute_test.go index b957cc73f..daff176a5 100644 --- a/provider/schema/number_attribute_test.go +++ b/provider/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestNumberAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestNumberAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestNumberAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestNumberAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestNumberAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -268,8 +257,6 @@ func TestNumberAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestNumberAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestNumberAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,6 +365,32 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() @@ -404,8 +411,6 @@ func TestNumberAttributeNumberValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +422,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index 3041f5a79..0cb33f1fd 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -197,6 +198,24 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/object_attribute_test.go b/provider/schema/object_attribute_test.go index 01f65b864..2ae9d0eec 100644 --- a/provider/schema/object_attribute_test.go +++ b/provider/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -64,8 +65,6 @@ func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -112,8 +111,6 @@ func TestObjectAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -152,8 +149,6 @@ func TestObjectAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -186,8 +181,6 @@ func TestObjectAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -220,8 +213,6 @@ func TestObjectAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -254,8 +245,6 @@ func TestObjectAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +271,6 @@ func TestObjectAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +303,6 @@ func TestObjectAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +335,6 @@ func TestObjectAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +367,6 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +379,32 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() @@ -418,8 +425,6 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -534,8 +539,6 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -548,3 +551,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/schema_test.go b/provider/schema/schema_test.go index 5b84aac8c..9007eabdc 100644 --- a/provider/schema/schema_test.go +++ b/provider/schema/schema_test.go @@ -100,8 +100,6 @@ func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,7 +267,6 @@ func TestSchemaAttributeAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,7 +387,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -448,8 +444,6 @@ func TestSchemaGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -504,8 +498,6 @@ func TestSchemaGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -542,8 +534,6 @@ func TestSchemaGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -580,8 +570,6 @@ func TestSchemaGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -618,8 +606,6 @@ func TestSchemaGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -650,8 +636,6 @@ func TestSchemaGetVersion(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -698,8 +682,6 @@ func TestSchemaType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -839,8 +821,6 @@ func TestSchemaTypeAtPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -972,8 +952,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1029,8 +1007,6 @@ func TestSchemaValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1342,8 +1318,6 @@ func TestSchemaValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index 3297452b7..9d1a8f0c0 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -193,6 +194,24 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_attribute_test.go b/provider/schema/set_attribute_test.go index 862b2d8dd..efa665df6 100644 --- a/provider/schema/set_attribute_test.go +++ b/provider/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -58,8 +59,6 @@ func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -106,8 +105,6 @@ func TestSetAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +143,6 @@ func TestSetAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +175,6 @@ func TestSetAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +207,6 @@ func TestSetAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +239,6 @@ func TestSetAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +265,6 @@ func TestSetAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -310,8 +297,6 @@ func TestSetAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -344,8 +329,6 @@ func TestSetAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +361,6 @@ func TestSetAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -392,6 +373,32 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() @@ -412,8 +419,6 @@ func TestSetAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -501,8 +506,6 @@ func TestSetAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -515,3 +518,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index 7a2fb6060..f5d788e6f 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -219,6 +219,24 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_nested_attribute_test.go b/provider/schema/set_nested_attribute_test.go index 163604a9c..6dd173469 100644 --- a/provider/schema/set_nested_attribute_test.go +++ b/provider/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -86,8 +87,6 @@ func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +139,6 @@ func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,8 +228,6 @@ func TestSetNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +266,6 @@ func TestSetNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -311,8 +304,6 @@ func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +340,6 @@ func TestSetNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +384,6 @@ func TestSetNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -429,8 +416,6 @@ func TestSetNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -469,8 +454,6 @@ func TestSetNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -509,8 +492,6 @@ func TestSetNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -549,8 +530,6 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -563,6 +542,32 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() @@ -589,8 +594,6 @@ func TestSetNestedAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -668,8 +671,6 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -682,3 +683,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/set_nested_block_test.go b/provider/schema/set_nested_block_test.go index d8a680009..14bd372de 100644 --- a/provider/schema/set_nested_block_test.go +++ b/provider/schema/set_nested_block_test.go @@ -86,8 +86,6 @@ func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -140,8 +138,6 @@ func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +256,6 @@ func TestSetNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -300,8 +294,6 @@ func TestSetNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +332,6 @@ func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -378,8 +368,6 @@ func TestSetNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestSetNestedBlockSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -476,8 +462,6 @@ func TestSetNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -554,8 +538,6 @@ func TestSetNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index 4aa669bf1..6c306d167 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -233,6 +233,24 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/single_nested_attribute_test.go b/provider/schema/single_nested_attribute_test.go index a471cbc20..a674a82a9 100644 --- a/provider/schema/single_nested_attribute_test.go +++ b/provider/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -81,8 +82,6 @@ func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +171,6 @@ func TestSingleNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -210,8 +207,6 @@ func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +243,6 @@ func TestSingleNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -286,8 +279,6 @@ func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -322,8 +313,6 @@ func TestSingleNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,8 +353,6 @@ func TestSingleNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +383,6 @@ func TestSingleNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -434,8 +419,6 @@ func TestSingleNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -472,8 +455,6 @@ func TestSingleNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -510,8 +491,6 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -524,6 +503,32 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() @@ -548,8 +553,6 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -561,3 +564,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/single_nested_block_test.go b/provider/schema/single_nested_block_test.go index 802f41be6..d4a7e75cd 100644 --- a/provider/schema/single_nested_block_test.go +++ b/provider/schema/single_nested_block_test.go @@ -99,8 +99,6 @@ func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +149,6 @@ func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +249,6 @@ func TestSingleNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -291,8 +285,6 @@ func TestSingleNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +321,6 @@ func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,8 +369,6 @@ func TestSingleNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,8 +405,6 @@ func TestSingleNestedBlockObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -471,8 +457,6 @@ func TestSingleNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index 7ab7a0c42..feb9e8039 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,6 +175,24 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/provider/schema/string_attribute_test.go b/provider/schema/string_attribute_test.go index 4d24ef73d..366f84116 100644 --- a/provider/schema/string_attribute_test.go +++ b/provider/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -55,8 +56,6 @@ func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -103,8 +102,6 @@ func TestStringAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -138,8 +135,6 @@ func TestStringAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -172,8 +167,6 @@ func TestStringAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -206,8 +199,6 @@ func TestStringAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -240,8 +231,6 @@ func TestStringAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -268,8 +257,6 @@ func TestStringAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -302,8 +289,6 @@ func TestStringAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +321,6 @@ func TestStringAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -370,8 +353,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,6 +365,32 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() @@ -404,8 +411,6 @@ func TestStringAttributeStringValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -417,3 +422,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/providerserver/serve_opts_test.go b/providerserver/serve_opts_test.go index 5b7634819..5df7e98f1 100644 --- a/providerserver/serve_opts_test.go +++ b/providerserver/serve_opts_test.go @@ -60,8 +60,6 @@ func TestServeOptsValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -111,8 +109,6 @@ func TestServeOptsValidateAddress(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/create.go b/resource/create.go index 8831f8370..920cc5dd3 100644 --- a/resource/create.go +++ b/resource/create.go @@ -23,6 +23,10 @@ type CreateRequest struct { // Plan is the planned state for the resource. Plan tfsdk.Plan + // Identity is the planned identity for the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config } @@ -37,6 +41,14 @@ type CreateResponse struct { // should be set during the resource's Create operation. State tfsdk.State + // Identity is the identity of the resource following the Create operation. + // This field is pre-populated from CreateRequest.Identity and + // should be set during the resource's Create operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Create operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Create operation. // This field is not pre-populated as there is no pre-existing private state // data during the resource's Create operation. diff --git a/resource/delete.go b/resource/delete.go index ab81a6c92..6644737d9 100644 --- a/resource/delete.go +++ b/resource/delete.go @@ -17,6 +17,10 @@ type DeleteRequest struct { // operation. State tfsdk.State + // Identity is the current identity of the resource prior to the Delete + // operation. If the resource does not support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config @@ -33,10 +37,13 @@ type DeleteRequest struct { // should set values on the DeleteResponse as appropriate. type DeleteResponse struct { // State is the state of the resource following the Delete operation. - // This field is pre-populated from UpdateResourceRequest.Plan and - // should be set during the resource's Update operation. + // This field is pre-populated from DeleteRequest.State and + // should be set during the resource's Delete operation. State tfsdk.State + // Identity is the identity of the resource following the Delete operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Delete // operation. This field is pre-populated from DeleteRequest.Private and // can be modified during the resource's Delete operation in cases where diff --git a/resource/identity_schema.go b/resource/identity_schema.go new file mode 100644 index 000000000..812173d20 --- /dev/null +++ b/resource/identity_schema.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" +) + +// IdentitySchemaRequest represents a request for the Resource to return its identity schema. +// An instance of this request struct is supplied as an argument to the +// Resource type IdentitySchema method. +type IdentitySchemaRequest struct{} + +// IdentitySchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the Resource type IdentitySchema +// method. +type IdentitySchemaResponse struct { + // IdentitySchema is the schema of the resource identity. + IdentitySchema identityschema.Schema + + // Diagnostics report errors or warnings related to retrieving the resource + // identity schema. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/resource/identityschema/attribute.go b/resource/identityschema/attribute.go new file mode 100644 index 000000000..06ef93150 --- /dev/null +++ b/resource/identityschema/attribute.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute define a value field inside the Schema. Implementations in this +// package include: +// - BoolAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - NumberAttribute +// - StringAttribute +// +// The available attribute types for a resource identity schema are intentionally +// limited. Nested attributes and blocks are not supported in identity schemas, +// as well as ListAttribute definitions can only have primitive element types of: +// - types.BoolType +// - types.Float32Type +// - types.Float64Type +// - types.Int32Type +// - types.Int64Type +// - types.NumberType +// - types.StringType +type Attribute interface { + fwschema.Attribute +} diff --git a/resource/identityschema/bool_attribute.go b/resource/identityschema/bool_attribute.go new file mode 100644 index 000000000..c17ae9549 --- /dev/null +++ b/resource/identityschema/bool_attribute.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.BoolType. When retrieving data, the basetypes.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType basetypes.BoolTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a BoolAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a BoolAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a BoolAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/bool_attribute_test.go b/resource/identityschema/bool_attribute_test.go new file mode 100644 index 000000000..057cde795 --- /dev/null +++ b/resource/identityschema/bool_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.BoolType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.BoolAttribute{}, + other: identityschema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-description": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: identityschema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.BoolAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.BoolAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/doc.go b/resource/identityschema/doc.go new file mode 100644 index 000000000..5e468a5cf --- /dev/null +++ b/resource/identityschema/doc.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package identityschema contains all available schema functionality for managed +// resource identity. +// +// Resource identity schemas define the structure and value types for identity state data. +// Schemas are implemented via the resource.ResourceWithIdentity type IdentitySchema method. +package identityschema diff --git a/resource/identityschema/float32_attribute.go b/resource/identityschema/float32_attribute.go new file mode 100644 index 000000000..ce741d32c --- /dev/null +++ b/resource/identityschema/float32_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Float32Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Float32Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Float32Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/float32_attribute_test.go b/resource/identityschema/float32_attribute_test.go new file mode 100644 index 000000000..0f906f88d --- /dev/null +++ b/resource/identityschema/float32_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Float32Attribute{}, + other: identityschema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: identityschema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Float32Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Float32Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/float64_attribute.go b/resource/identityschema/float64_attribute.go new file mode 100644 index 000000000..a6e33fb3c --- /dev/null +++ b/resource/identityschema/float64_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float64Type. When retrieving data, the basetypes.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType basetypes.Float64Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Float64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Float64Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Float64Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/float64_attribute_test.go b/resource/identityschema/float64_attribute_test.go new file mode 100644 index 000000000..e5473e7b3 --- /dev/null +++ b/resource/identityschema/float64_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Float64Attribute{}, + other: identityschema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: identityschema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Float64Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Float64Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/int32_attribute.go b/resource/identityschema/int32_attribute.go new file mode 100644 index 000000000..a5ef7e819 --- /dev/null +++ b/resource/identityschema/int32_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Int32Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Int32Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Int32Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/int32_attribute_test.go b/resource/identityschema/int32_attribute_test.go new file mode 100644 index 000000000..7b68255c2 --- /dev/null +++ b/resource/identityschema/int32_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Int32Attribute{}, + other: identityschema.Int32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: identityschema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Int32Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Int32Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/int64_attribute.go b/resource/identityschema/int64_attribute.go new file mode 100644 index 000000000..332a1d1c1 --- /dev/null +++ b/resource/identityschema/int64_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +type Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int64Type. When retrieving data, the basetypes.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType basetypes.Int64Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Int64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Int64Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Int64Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/int64_attribute_test.go b/resource/identityschema/int64_attribute_test.go new file mode 100644 index 000000000..2bba9c500 --- /dev/null +++ b/resource/identityschema/int64_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Int64Attribute{}, + other: identityschema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Int64Attribute{}, + expected: types.Int64Type, + }, + "custom-type": { + attribute: identityschema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Int64Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Int64Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/list_attribute.go b/resource/identityschema/list_attribute.go new file mode 100644 index 000000000..185a3bba8 --- /dev/null +++ b/resource/identityschema/list_attribute.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// In identity schemas, ListAttribute is only permitted to have a primitive ElementType, +// which are: +// - types.BoolType +// - types.Float32Type +// - types.Float64Type +// - types.Int32Type +// - types.Int64Type +// - types.NumberType +// - types.StringType +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // ElementType must be a primitive, which are: + // - types.BoolType + // - types.Float32Type + // - types.Float64Type + // - types.Int32Type + // - types.Int64Type + // - types.NumberType + // - types.StringType + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ListType. When retrieving data, the basetypes.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType basetypes.ListTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a ListAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a ListAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a ListAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a ListAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetResourceIdentitySchemas RPC and +// should never include false positives. +func (a ListAttribute) ValidateImplementation(_ context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + return + } + + if a.CustomType == nil && !fwtype.IsAllowedPrimitiveType(a.ElementType) { + resp.Diagnostics.Append(fwschema.AttributeInvalidElementTypeDiag(req.Path, a.ElementType)) + } +} diff --git a/resource/identityschema/list_attribute_test.go b/resource/identityschema/list_attribute_test.go new file mode 100644 index 000000000..4affa8886 --- /dev/null +++ b/resource/identityschema/list_attribute_test.go @@ -0,0 +1,626 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: identityschema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: identityschema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-description": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: identityschema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + "custom-type": { + attribute: identityschema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "elementtype": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-missing": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + "elementtype-bool": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.BoolType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-int64": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.Int64Type, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-int32": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.Int32Type, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-float64": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.Float64Type, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-float32": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.Float32Type, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-number": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.NumberType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-notprimitive-dynamic": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.DynamicType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" contains an element of type \"basetypes.DynamicType\" that is not allowed for Lists in Resource Identity. "+ + "Lists in Resource Identity may only have primitive element types such as Bool, Int, Float, Number and String.", + ), + }, + }, + }, + "elementtype-notprimitive-object": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.ObjectType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" contains an element of type \"types.ObjectType[]\" that is not allowed for Lists in Resource Identity. "+ + "Lists in Resource Identity may only have primitive element types such as Bool, Int, Float, Number and String.", + ), + }, + }, + }, + "elementtype-notprimitive-map": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" contains an element of type \"types.MapType[!!! MISSING TYPE !!!]\" that is not allowed for Lists in Resource Identity. "+ + "Lists in Resource Identity may only have primitive element types such as Bool, Int, Float, Number and String.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/number_attribute.go b/resource/identityschema/number_attribute.go new file mode 100644 index 000000000..d1533477a --- /dev/null +++ b/resource/identityschema/number_attribute.go @@ -0,0 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.NumberType. When retrieving data, the basetypes.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType basetypes.NumberTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a NumberAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a NumberAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a NumberAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/number_attribute_test.go b/resource/identityschema/number_attribute_test.go new file mode 100644 index 000000000..9597643cd --- /dev/null +++ b/resource/identityschema/number_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.NumberType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.NumberAttribute{}, + other: identityschema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-description": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: identityschema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.NumberAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.NumberAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/schema.go b/resource/identityschema/schema.go new file mode 100644 index 000000000..6d68aff9a --- /dev/null +++ b/resource/identityschema/schema.go @@ -0,0 +1,145 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Schema must satify the fwschema.Schema interface. +var _ fwschema.Schema = Schema{} + +// Schema defines the structure and value types of resource identity data. This type +// is used as the resource.IdentitySchemaResponse type IdentitySchema field, which is +// implemented by the resource.ResourceWithIdentity type IdentitySchema method. +type Schema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + Attributes map[string]Attribute + + // Version indicates the current version of the resource identity schema. Resource + // identity schema versioning enables identity state upgrades in conjunction with the + // [resource.ResourceWithUpgradeIdentity] interface. Versioning is only + // required if there is a breaking change involving existing identity state data, + // such as changing an attribute type in a manner that is incompatible with the Terraform type. + // + // Versions are conventionally only incremented by one each release. + Version int64 +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks returns an empty map as it's not relevant for identity schemas. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return map[string]fwschema.Block{} +} + +// GetDeprecationMessage returns an empty string as identity schemas cannot +// surface deprecation messages. +func (s Schema) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns an empty string as identity schemas cannot +// surface descriptions. +func (s Schema) GetDescription() string { + return "" +} + +// GetMarkdownDescription returns an empty string as identity schemas cannot +// surface descriptions. +func (s Schema) GetMarkdownDescription() string { + return "" +} + +// GetVersion returns the Version field value. +func (s Schema) GetVersion() int64 { + return s.Version +} + +// Type returns the framework type of the schema. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath returns the framework type at the given tftypes path. +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +// +// Deprecated: Use the ValidateImplementation method instead. +func (s Schema) Validate() diag.Diagnostics { + return s.ValidateImplementation(context.Background()) +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the schema and underlying attributes and blocks to prevent +// unexpected errors or panics. This logic runs during the +// GetResourceIdentitySchemas RPC, or via provider-defined unit testing, and should +// never include false positives. +func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + for attributeName, attribute := range s.GetAttributes() { + req := fwschema.ValidateImplementationRequest{ + Name: attributeName, + Path: path.Root(attributeName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...) + } + + return diags +} + +// schemaAttributes is a identity to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} diff --git a/resource/identityschema/schema_test.go b/resource/identityschema/schema_test.go new file mode 100644 index 000000000..394cdd426 --- /dev/null +++ b/resource/identityschema/schema_test.go @@ -0,0 +1,915 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: identityschema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: identityschema.Schema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type identityschema.Schema", + ), + }, + }, + "root": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type identityschema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "other": identityschema.BoolAttribute{}, + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: identityschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "empty-nil": { + schema: identityschema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "root": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "nil": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "WithAttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "other": identityschema.BoolAttribute{}, + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: identityschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: identityschema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr1": identityschema.StringAttribute{}, + "testattr2": identityschema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": identityschema.StringAttribute{}, + "testattr2": identityschema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: identityschema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-deprecation-message": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-description": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-markdown-description": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected int64 + }{ + "no-version": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: 0, + }, + "version": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + Version: 1, + }, + expected: 1, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected attr.Type + }{ + "base": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: identityschema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: identityschema.Schema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: identityschema.Schema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: identityschema.Schema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: identityschema.Schema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: identityschema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: identityschema.Schema{}, + }, + "validate-implementation-error": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "depends_on": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: identityschema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "depends_on": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "^": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "attribute-with-validate-attribute-implementation-error": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.ListAttribute{ + RequiredForImport: true, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/resource/identityschema/string_attribute.go b/resource/identityschema/string_attribute.go new file mode 100644 index 000000000..3061b89cf --- /dev/null +++ b/resource/identityschema/string_attribute.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.StringType. When retrieving data, the basetypes.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType basetypes.StringTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a StringAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a StringAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a StringAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a StringAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/string_attribute_test.go b/resource/identityschema/string_attribute_test.go new file mode 100644 index 000000000..18f6666e6 --- /dev/null +++ b/resource/identityschema/string_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.StringType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.StringAttribute{}, + other: identityschema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-description": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: identityschema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.StringAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.StringAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/import_state.go b/resource/import_state.go index 63f1b9fc5..2c8e0ea1a 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) // ImportStateClientCapabilities allows Terraform to publish information @@ -34,8 +35,19 @@ type ImportStateRequest struct { // as an Attribute. However, this identifier can also be treated as // its own type of value and parsed during import. This value // is not stored in the state unless the provider explicitly stores it. + // + // This ID field is supplied in the "terraform import" CLI command or in the import config block "id" attribute. + // Either ID or Identity must be supplied by the practitioner, depending on the method used to import the resource. ID string + // Identity is the configuration data provided by the practitioner in the import config block "identity" attribute. This + // configuration data will conform to the identity schema defined by the managed resource. If the resource does not support identity, + // this value will not be set. + // + // The "identity" attribute in the import block is only supported in Terraform 1.12 and later. + // Either ID or Identity must be supplied by the practitioner, depending on the method used to import the resource. + Identity *tfsdk.ResourceIdentity + // ClientCapabilities defines optionally supported protocol features for the // ImportResourceState RPC, such as forward-compatible Terraform behavior changes. ClientCapabilities ImportStateClientCapabilities @@ -56,6 +68,14 @@ type ImportStateResponse struct { // refresh the resource, e.g. call the Resource Read method. State tfsdk.State + // Identity is the identity of the resource following the Import operation. + // This field is pre-populated from ImportStateRequest.Identity and + // should be set during the resource's Import operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Import operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Import operation. // This field is not pre-populated as there is no pre-existing private state // data during the resource's Import operation. @@ -75,6 +95,11 @@ type ImportStateResponse struct { // ImportStatePassthroughID is a helper function to set the import // identifier to a given state attribute path. The attribute must accept a // string value. +// +// For resources that support identity, this method will also automatically pass through the +// Identity field if imported by the identity attribute of a import config block (Terraform 1.12+ and later). +// In this scenario where identity is provided instead of the string ID, the state field defined +// at `attrPath` will be set to null. func ImportStatePassthroughID(ctx context.Context, attrPath path.Path, req ImportStateRequest, resp *ImportStateResponse) { if attrPath.Equal(path.Empty()) { resp.Diagnostics.AddError( @@ -82,7 +107,59 @@ func ImportStatePassthroughID(ctx context.Context, attrPath path.Path, req Impor "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ "Resource ImportState method call to ImportStatePassthroughID path must be set to a valid attribute path that can accept a string value.", ) + return + } + + // If the import is using the ID string identifier, (either via the "terraform import" CLI command, or a config block with the "id" attribute set) + // pass through the ID to the designated state attribute. + if req.ID != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, attrPath, req.ID)...) + } +} + +// ImportStatePassthroughWithIdentity is a helper function to retrieve either the import identifier +// or a given identity attribute that is then used to set to given attribute path in state, based on the method used +// by the practitioner to import. The identity and state attributes provided must be of type string. +// +// The helper method should only be used on resources that support identity via the resource.ResourceWithIdentity interface. +// +// This method will also automatically pass through the Identity field if imported by +// the identity attribute of a import config block (Terraform 1.12+ and later). +func ImportStatePassthroughWithIdentity(ctx context.Context, stateAttrPath, identityAttrPath path.Path, req ImportStateRequest, resp *ImportStateResponse) { + if stateAttrPath.Equal(path.Empty()) { + resp.Diagnostics.AddError( + "Resource Import Passthrough Missing State Attribute Path", + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Resource ImportState method call to ImportStatePassthroughWithIdentity path must be set to a valid state attribute path that can accept a string value.", + ) + } + + if identityAttrPath.Equal(path.Empty()) { + resp.Diagnostics.AddError( + "Resource Import Passthrough Missing Identity Attribute Path", + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Resource ImportState method call to ImportStatePassthroughWithIdentity path must be set to a valid identity attribute path that is a string value.", + ) + } + + if resp.Diagnostics.HasError() { + return + } + + // If the import is using the import identifier, (either via the "terraform import" CLI command, or a config block with the "id" attribute set) + // pass through the ID to the designated state attribute. + if req.ID != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, stateAttrPath, req.ID)...) + return + } + + // The import isn't using the import identifier, so it must be using identity. Grab the designated + // identity attribute string and set it to state. + var identityAttrVal types.String + resp.Diagnostics.Append(req.Identity.GetAttribute(ctx, identityAttrPath, &identityAttrVal)...) + if resp.Diagnostics.HasError() { + return } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, attrPath, req.ID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, stateAttrPath, identityAttrVal)...) } diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 28843cec1..0cc71bea9 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -35,6 +35,10 @@ type ModifyPlanRequest struct { // State is the current state of the resource. State tfsdk.State + // Identity is the current identity of the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // Plan is the planned new state for the resource. Terraform 1.3 and later // supports resource destroy planning, in which this will contain a null // value. @@ -65,6 +69,13 @@ type ModifyPlanResponse struct { // Plan is the planned new state for the resource. Plan tfsdk.Plan + // Identity is the planned new identity of the resource. + // This field is pre-populated from ModifyPlanRequest.Identity. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set. + Identity *tfsdk.ResourceIdentity + // RequiresReplace is a list of attribute paths that require the // resource to be replaced. They should point to the specific field // that changed that requires the resource to be destroyed and diff --git a/resource/move_state.go b/resource/move_state.go index 6918f5b76..977f565d9 100644 --- a/resource/move_state.go +++ b/resource/move_state.go @@ -75,6 +75,14 @@ type MoveStateRequest struct { // other request fields, to determine whether the request should be handled // by this particular implementation. SourceTypeName string + + // SourceIdentity is the identity of the source resource. + // + // Only the underlying JSON field is populated. + SourceIdentity *tfprotov6.RawState + + // SourceIdentitySchemaVersion is the version of the source resource identity. + SourceIdentitySchemaVersion int64 } // MoveStateResponse represents a response to a MoveStateRequest. @@ -107,4 +115,7 @@ type MoveStateResponse struct { // operation. This field is not pre-populated as it is up to implementations // whether the source private data is relevant for the target resource. TargetPrivate *privatestate.ProviderData + + // TargetIdentity is the identity of the target resource. + TargetIdentity *tfsdk.ResourceIdentity } diff --git a/resource/read.go b/resource/read.go index 53c4cb832..74820e333 100644 --- a/resource/read.go +++ b/resource/read.go @@ -30,6 +30,10 @@ type ReadRequest struct { // operation. State tfsdk.State + // Identity is the current identity of the resource prior to the Read + // operation. If the resource does not support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // Private is provider-defined resource private state data which was previously // stored with the resource state. This data is opaque to Terraform and does // not affect plan output. Any existing data is copied to @@ -57,6 +61,14 @@ type ReadResponse struct { // should be set during the resource's Read operation. State tfsdk.State + // Identity is the identity of the resource following the Read operation. + // This field is pre-populated from ReadRequest.Identity and + // should be set during the resource's Read operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Read operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Read operation. // This field is pre-populated from ReadResourceRequest.Private and // can be modified during the resource's Read operation. diff --git a/resource/resource.go b/resource/resource.go index 7113d4bf5..2cf334a77 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -19,6 +19,9 @@ import ( // - Plan Modification: Schema-based or entire plan // via ResourceWithModifyPlan. // - State Upgrades: ResourceWithUpgradeState +// - Identity: Define an identity schema with ResourceWithIdentity to enable +// storing identity data in state during CRUD operations. Identity data is +// used by Terraform to uniquely identify a managed resource. // // Although not required, it is conventional for resources to implement the // ResourceWithImportState interface. @@ -196,3 +199,27 @@ type ResourceWithValidateConfig interface { // ValidateConfig performs the validation. ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } + +// ResourceWithIdentity is an interface type that extends Resource to implement managed resource identity. +// +// Managed resources can optionally define an identity schema, which represents a separate object stored in state +// alongside the resource instance data. This identity data is used by Terraform to uniquely identify +// managed resources and has additional restrictions that allow external programs to determine equality +// between two identities. +// +// Resource identity schemas can only contain primitive (bool, string, number) attributes and lists that +// contain primitive elements. Additionally, a resource identity should have the following properties: +// - The resource identity should correspond to at most one remote object per provider, across all +// instances of that provider. +// - Given a resource identity, the provider should be able to determine whether the corresponding remote +// object exists, and if so, return the resource state. Resources that support identity can be imported +// by the identity object via the ResourceWithImportState interface. +// - The resource identity should not change during the lifecycle of the remote object. That is, from the +// creation of the remote object in the remote system until its destruction. An exception to this rule +// is an upgrade of the identity data after a schema change, via the ResourceWithUpgradeIdentity interface. +type ResourceWithIdentity interface { + Resource + + // IdentitySchema should return the identity schema for this resource. + IdentitySchema(context.Context, IdentitySchemaRequest, *IdentitySchemaResponse) +} diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index abb0b8708..3cdbc0e9f 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -152,6 +152,16 @@ type BoolAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Bool + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -229,6 +239,23 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a BoolAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/bool_attribute_test.go b/resource/schema/bool_attribute_test.go index d4b85ef2d..e260c76d6 100644 --- a/resource/schema/bool_attribute_test.go +++ b/resource/schema/bool_attribute_test.go @@ -62,8 +62,6 @@ func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestBoolAttributeBoolDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -157,8 +153,6 @@ func TestBoolAttributeBoolPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -191,8 +185,6 @@ func TestBoolAttributeBoolValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -225,8 +217,6 @@ func TestBoolAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +250,6 @@ func TestBoolAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +282,6 @@ func TestBoolAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestBoolAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -362,8 +346,6 @@ func TestBoolAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +378,6 @@ func TestBoolAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -430,8 +410,6 @@ func TestBoolAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +442,6 @@ func TestBoolAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +474,6 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -512,6 +486,38 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } } +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.BoolAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBoolAttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestBoolAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestBoolAttributeValidateImplementation(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/booldefault/static_value_test.go b/resource/schema/booldefault/static_value_test.go index a346d57d8..8d79eb88d 100644 --- a/resource/schema/booldefault/static_value_test.go +++ b/resource/schema/booldefault/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticBoolDefaultBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/boolplanmodifier/requires_replace_if_configured_test.go b/resource/schema/boolplanmodifier/requires_replace_if_configured_test.go index b4559f5a6..13bfae26b 100644 --- a/resource/schema/boolplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/boolplanmodifier/requires_replace_if_configured_test.go @@ -151,8 +151,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/boolplanmodifier/requires_replace_if_test.go b/resource/schema/boolplanmodifier/requires_replace_if_test.go index 73a941856..4d88e36ef 100644 --- a/resource/schema/boolplanmodifier/requires_replace_if_test.go +++ b/resource/schema/boolplanmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/boolplanmodifier/requires_replace_test.go b/resource/schema/boolplanmodifier/requires_replace_test.go index bb92ca4d0..d73c2b2da 100644 --- a/resource/schema/boolplanmodifier/requires_replace_test.go +++ b/resource/schema/boolplanmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/boolplanmodifier/use_state_for_unknown_test.go b/resource/schema/boolplanmodifier/use_state_for_unknown_test.go index cad1f649e..c385c8c75 100644 --- a/resource/schema/boolplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/boolplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index 7b97625d9..b7b4f38b5 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -153,6 +153,16 @@ type DynamicAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Dynamic + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -215,6 +225,23 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a DynamicAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} + // DynamicDefaultValue returns the Default field value. func (a DynamicAttribute) DynamicDefaultValue() defaults.Dynamic { return a.Default diff --git a/resource/schema/dynamic_attribute_test.go b/resource/schema/dynamic_attribute_test.go index f99dc598c..4bacf972d 100644 --- a/resource/schema/dynamic_attribute_test.go +++ b/resource/schema/dynamic_attribute_test.go @@ -62,8 +62,6 @@ func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestDynamicAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +141,6 @@ func TestDynamicAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +173,6 @@ func TestDynamicAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,8 +205,6 @@ func TestDynamicAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +237,6 @@ func TestDynamicAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -281,8 +269,6 @@ func TestDynamicAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -315,8 +301,6 @@ func TestDynamicAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +333,6 @@ func TestDynamicAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -383,8 +365,6 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -397,6 +377,38 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.DynamicAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicDefaultValue(t *testing.T) { t.Parallel() @@ -430,8 +442,6 @@ func TestDynamicAttributeDynamicDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +474,6 @@ func TestDynamicAttributeDynamicPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +506,6 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestDynamicAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestDynamicAttributeValidateImplementation(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/dynamicdefault/static_value_test.go b/resource/schema/dynamicdefault/static_value_test.go index d96670b51..825e4d646 100644 --- a/resource/schema/dynamicdefault/static_value_test.go +++ b/resource/schema/dynamicdefault/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticValueDefaultDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamicplanmodifier/requires_replace_if_configured_test.go b/resource/schema/dynamicplanmodifier/requires_replace_if_configured_test.go index fc017e69e..8f26f5b69 100644 --- a/resource/schema/dynamicplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/dynamicplanmodifier/requires_replace_if_configured_test.go @@ -164,8 +164,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamicplanmodifier/requires_replace_if_test.go b/resource/schema/dynamicplanmodifier/requires_replace_if_test.go index c482bcab8..a878405f2 100644 --- a/resource/schema/dynamicplanmodifier/requires_replace_if_test.go +++ b/resource/schema/dynamicplanmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamicplanmodifier/requires_replace_test.go b/resource/schema/dynamicplanmodifier/requires_replace_test.go index 2b1d76fb4..6fad7bac8 100644 --- a/resource/schema/dynamicplanmodifier/requires_replace_test.go +++ b/resource/schema/dynamicplanmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go b/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go index 13fbcd264..58e346bf1 100644 --- a/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go @@ -140,8 +140,6 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index 9e8e7a22a..acd37a157 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -155,6 +155,16 @@ type Float32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float32 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,23 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float32_attribute_test.go b/resource/schema/float32_attribute_test.go index 75f1a9637..18b8d1541 100644 --- a/resource/schema/float32_attribute_test.go +++ b/resource/schema/float32_attribute_test.go @@ -62,8 +62,6 @@ func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestFloat32AttributeFloat32DefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -157,8 +153,6 @@ func TestFloat32AttributeFloat32PlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -191,8 +185,6 @@ func TestFloat32AttributeFloat32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -225,8 +217,6 @@ func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +250,6 @@ func TestFloat32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +282,6 @@ func TestFloat32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -362,8 +346,6 @@ func TestFloat32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +378,6 @@ func TestFloat32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -430,8 +410,6 @@ func TestFloat32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +442,6 @@ func TestFloat32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +474,6 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -512,6 +486,38 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } } +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat32AttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestFloat32AttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestFloat32AttributeValidateImplementation(t *testing.T) { }) } } + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32default/static_value_test.go b/resource/schema/float32default/static_value_test.go index 543677b05..9e61e4309 100644 --- a/resource/schema/float32default/static_value_test.go +++ b/resource/schema/float32default/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticFloat32DefaultFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32planmodifier/requires_replace_if_configured_test.go b/resource/schema/float32planmodifier/requires_replace_if_configured_test.go index 2d13f3d97..ef437a77c 100644 --- a/resource/schema/float32planmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/float32planmodifier/requires_replace_if_configured_test.go @@ -151,8 +151,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32planmodifier/requires_replace_if_test.go b/resource/schema/float32planmodifier/requires_replace_if_test.go index f8185a66f..cc8c07e04 100644 --- a/resource/schema/float32planmodifier/requires_replace_if_test.go +++ b/resource/schema/float32planmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32planmodifier/requires_replace_test.go b/resource/schema/float32planmodifier/requires_replace_test.go index 32f35e86c..02252e4ee 100644 --- a/resource/schema/float32planmodifier/requires_replace_test.go +++ b/resource/schema/float32planmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32planmodifier/use_state_for_unknown_test.go b/resource/schema/float32planmodifier/use_state_for_unknown_test.go index cb8c514fa..2151333aa 100644 --- a/resource/schema/float32planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/float32planmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 7d762a4a2..8db8fb0be 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -155,6 +155,16 @@ type Float64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float64 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,23 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float64_attribute_test.go b/resource/schema/float64_attribute_test.go index a7d155c9f..81d2ea52b 100644 --- a/resource/schema/float64_attribute_test.go +++ b/resource/schema/float64_attribute_test.go @@ -62,8 +62,6 @@ func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -123,8 +121,6 @@ func TestFloat64AttributeFloat64DefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -157,8 +153,6 @@ func TestFloat64AttributeFloat64PlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -191,8 +185,6 @@ func TestFloat64AttributeFloat64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -225,8 +217,6 @@ func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +250,6 @@ func TestFloat64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +282,6 @@ func TestFloat64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -362,8 +346,6 @@ func TestFloat64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +378,6 @@ func TestFloat64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -430,8 +410,6 @@ func TestFloat64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +442,6 @@ func TestFloat64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +474,6 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -512,6 +486,38 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } } +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat64AttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestFloat64AttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestFloat64AttributeValidateImplementation(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float64default/static_value_test.go b/resource/schema/float64default/static_value_test.go index 03feb33c1..923a0fe6d 100644 --- a/resource/schema/float64default/static_value_test.go +++ b/resource/schema/float64default/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticFloat64DefaultFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64planmodifier/requires_replace_if_configured_test.go b/resource/schema/float64planmodifier/requires_replace_if_configured_test.go index 2823b6482..79b567a26 100644 --- a/resource/schema/float64planmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/float64planmodifier/requires_replace_if_configured_test.go @@ -151,8 +151,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64planmodifier/requires_replace_if_test.go b/resource/schema/float64planmodifier/requires_replace_if_test.go index eec7cbdb2..15fec9b42 100644 --- a/resource/schema/float64planmodifier/requires_replace_if_test.go +++ b/resource/schema/float64planmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64planmodifier/requires_replace_test.go b/resource/schema/float64planmodifier/requires_replace_test.go index 5899e327e..172cf5ca4 100644 --- a/resource/schema/float64planmodifier/requires_replace_test.go +++ b/resource/schema/float64planmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64planmodifier/use_state_for_unknown_test.go b/resource/schema/float64planmodifier/use_state_for_unknown_test.go index d8d9168c5..75f439847 100644 --- a/resource/schema/float64planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/float64planmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index 41b74bcf3..5426a5743 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -155,6 +155,16 @@ type Int32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int32 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,23 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int32_attribute_test.go b/resource/schema/int32_attribute_test.go index 152d957f7..2cd74f5d1 100644 --- a/resource/schema/int32_attribute_test.go +++ b/resource/schema/int32_attribute_test.go @@ -62,8 +62,6 @@ func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestInt32AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +141,6 @@ func TestInt32AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +173,6 @@ func TestInt32AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,8 +205,6 @@ func TestInt32AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +237,6 @@ func TestInt32AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +282,6 @@ func TestInt32AttributeInt32DefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestInt32AttributeInt32PlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -362,8 +346,6 @@ func TestInt32AttributeInt32Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +378,6 @@ func TestInt32AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -430,8 +410,6 @@ func TestInt32AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +442,6 @@ func TestInt32AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +474,6 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -512,6 +486,38 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } } +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt32AttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestInt32AttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestInt32AttributeValidateImplementation(t *testing.T) { }) } } + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32default/static_value_test.go b/resource/schema/int32default/static_value_test.go index 6b34cd9d7..2fed56939 100644 --- a/resource/schema/int32default/static_value_test.go +++ b/resource/schema/int32default/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticInt32DefaultInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32planmodifier/requires_replace_if_configured_test.go b/resource/schema/int32planmodifier/requires_replace_if_configured_test.go index 4f6873025..6357e014b 100644 --- a/resource/schema/int32planmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/int32planmodifier/requires_replace_if_configured_test.go @@ -152,8 +152,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32planmodifier/requires_replace_if_test.go b/resource/schema/int32planmodifier/requires_replace_if_test.go index 950c94f47..6bc3cc165 100644 --- a/resource/schema/int32planmodifier/requires_replace_if_test.go +++ b/resource/schema/int32planmodifier/requires_replace_if_test.go @@ -163,8 +163,6 @@ func TestRequiresReplaceIfModifierPlanModifyInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32planmodifier/requires_replace_test.go b/resource/schema/int32planmodifier/requires_replace_test.go index 71881c8a6..8f1ea86e9 100644 --- a/resource/schema/int32planmodifier/requires_replace_test.go +++ b/resource/schema/int32planmodifier/requires_replace_test.go @@ -135,8 +135,6 @@ func TestRequiresReplaceModifierPlanModifyInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32planmodifier/use_state_for_unknown_test.go b/resource/schema/int32planmodifier/use_state_for_unknown_test.go index 6cb27e802..45bdc2b10 100644 --- a/resource/schema/int32planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/int32planmodifier/use_state_for_unknown_test.go @@ -122,8 +122,6 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index 65ec795e9..b700f2cca 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -155,6 +155,16 @@ type Int64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int64 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,23 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int64_attribute_test.go b/resource/schema/int64_attribute_test.go index a961c1fb8..36b29096e 100644 --- a/resource/schema/int64_attribute_test.go +++ b/resource/schema/int64_attribute_test.go @@ -62,8 +62,6 @@ func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestInt64AttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +141,6 @@ func TestInt64AttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +173,6 @@ func TestInt64AttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,8 +205,6 @@ func TestInt64AttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +237,6 @@ func TestInt64AttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +282,6 @@ func TestInt64AttributeInt64DefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -328,8 +314,6 @@ func TestInt64AttributeInt64PlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -362,8 +346,6 @@ func TestInt64AttributeInt64Validators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -396,8 +378,6 @@ func TestInt64AttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -430,8 +410,6 @@ func TestInt64AttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +442,6 @@ func TestInt64AttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +474,6 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -512,6 +486,38 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } } +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt64AttributeValidateImplementation(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestInt64AttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestInt64AttributeValidateImplementation(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int64default/static_value_test.go b/resource/schema/int64default/static_value_test.go index b6dbce3f1..13404c8fc 100644 --- a/resource/schema/int64default/static_value_test.go +++ b/resource/schema/int64default/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticInt64DefaultInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64planmodifier/requires_replace_if_configured_test.go b/resource/schema/int64planmodifier/requires_replace_if_configured_test.go index 23bbdb2ed..915e1b302 100644 --- a/resource/schema/int64planmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/int64planmodifier/requires_replace_if_configured_test.go @@ -151,8 +151,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64planmodifier/requires_replace_if_test.go b/resource/schema/int64planmodifier/requires_replace_if_test.go index 9c9a05451..f19b794b0 100644 --- a/resource/schema/int64planmodifier/requires_replace_if_test.go +++ b/resource/schema/int64planmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64planmodifier/requires_replace_test.go b/resource/schema/int64planmodifier/requires_replace_test.go index dcb819ead..92298f8a8 100644 --- a/resource/schema/int64planmodifier/requires_replace_test.go +++ b/resource/schema/int64planmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64planmodifier/use_state_for_unknown_test.go b/resource/schema/int64planmodifier/use_state_for_unknown_test.go index bd7e9279a..c588e5e67 100644 --- a/resource/schema/int64planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/int64planmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 1dc0e0e8c..29419ebd1 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -168,6 +168,16 @@ type ListAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a list @@ -232,6 +242,23 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListDefaultValue returns the Default field value. func (a ListAttribute) ListDefaultValue() defaults.List { return a.Default diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go index 784ec70d6..788ee84fe 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -63,8 +63,6 @@ func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -111,8 +109,6 @@ func TestListAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +147,6 @@ func TestListAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +179,6 @@ func TestListAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -219,8 +211,6 @@ func TestListAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +243,6 @@ func TestListAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestListAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -321,8 +307,6 @@ func TestListAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -355,8 +339,6 @@ func TestListAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -389,8 +371,6 @@ func TestListAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -403,6 +383,38 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListDefaultValue(t *testing.T) { t.Parallel() @@ -450,8 +462,6 @@ func TestListAttributeListDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -484,8 +494,6 @@ func TestListAttributeListPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -518,8 +526,6 @@ func TestListAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -714,8 +720,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -728,3 +732,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 95fd2ba01..76a4e3cdb 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -178,6 +178,19 @@ type ListNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +273,23 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListDefaultValue returns the Default field value. func (a ListNestedAttribute) ListDefaultValue() defaults.List { return a.Default @@ -284,6 +314,14 @@ func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fws resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ListDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go index a73c7a843..e1eaa3683 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -91,8 +91,6 @@ func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestListNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -236,8 +232,6 @@ func TestListNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +270,6 @@ func TestListNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +308,6 @@ func TestListNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -354,8 +344,6 @@ func TestListNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -400,8 +388,6 @@ func TestListNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -440,8 +426,6 @@ func TestListNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -480,8 +464,6 @@ func TestListNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -520,8 +502,6 @@ func TestListNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -560,8 +540,6 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -574,6 +552,38 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListDefaultValue(t *testing.T) { t.Parallel() @@ -621,8 +631,6 @@ func TestListNestedAttributeListDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -661,8 +669,6 @@ func TestListNestedAttributeListPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -701,8 +707,6 @@ func TestListNestedAttributeListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -909,11 +913,97 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -926,3 +1016,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_nested_block_test.go b/resource/schema/list_nested_block_test.go index c58551a04..e500287c1 100644 --- a/resource/schema/list_nested_block_test.go +++ b/resource/schema/list_nested_block_test.go @@ -87,8 +87,6 @@ func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -141,8 +139,6 @@ func TestListNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +257,6 @@ func TestListNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +295,6 @@ func TestListNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -341,8 +333,6 @@ func TestListNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,8 +369,6 @@ func TestListNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -419,8 +407,6 @@ func TestListNestedBlockListPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +445,6 @@ func TestListNestedBlockListValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -517,8 +501,6 @@ func TestListNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -569,8 +551,6 @@ func TestListNestedBlockValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/listdefault/static_value_test.go b/resource/schema/listdefault/static_value_test.go index 0ca83bcb3..0d0c8e50a 100644 --- a/resource/schema/listdefault/static_value_test.go +++ b/resource/schema/listdefault/static_value_test.go @@ -41,8 +41,6 @@ func TestStaticValueDefaultList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/listplanmodifier/requires_replace_if_configured_test.go b/resource/schema/listplanmodifier/requires_replace_if_configured_test.go index 55bb3d0a8..a7552a119 100644 --- a/resource/schema/listplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/listplanmodifier/requires_replace_if_configured_test.go @@ -154,8 +154,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/listplanmodifier/requires_replace_if_test.go b/resource/schema/listplanmodifier/requires_replace_if_test.go index 40887f128..2f6aa2be9 100644 --- a/resource/schema/listplanmodifier/requires_replace_if_test.go +++ b/resource/schema/listplanmodifier/requires_replace_if_test.go @@ -165,8 +165,6 @@ func TestRequiresReplaceIfModifierPlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/listplanmodifier/requires_replace_test.go b/resource/schema/listplanmodifier/requires_replace_test.go index 32ac458f7..6961bec81 100644 --- a/resource/schema/listplanmodifier/requires_replace_test.go +++ b/resource/schema/listplanmodifier/requires_replace_test.go @@ -137,8 +137,6 @@ func TestRequiresReplaceModifierPlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/listplanmodifier/use_state_for_unknown_test.go b/resource/schema/listplanmodifier/use_state_for_unknown_test.go index 6a38f8221..e41b07cac 100644 --- a/resource/schema/listplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/listplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index ac50f63f8..837b31100 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -171,6 +171,16 @@ type MapAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a map @@ -235,6 +245,23 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapDefaultValue returns the Default field value. func (a MapAttribute) MapDefaultValue() defaults.Map { return a.Default diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go index 4f56036d5..ce5bfb7da 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -63,8 +63,6 @@ func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -111,8 +109,6 @@ func TestMapAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +147,6 @@ func TestMapAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +179,6 @@ func TestMapAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -219,8 +211,6 @@ func TestMapAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +243,6 @@ func TestMapAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestMapAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -321,8 +307,6 @@ func TestMapAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -355,8 +339,6 @@ func TestMapAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -389,8 +371,6 @@ func TestMapAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -403,6 +383,38 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapDefaultValue(t *testing.T) { t.Parallel() @@ -450,8 +462,6 @@ func TestMapAttributeMapDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -484,8 +494,6 @@ func TestMapAttributeMapPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -517,8 +525,6 @@ func TestMapAttributeMapValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -713,8 +719,6 @@ func TestMapAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -727,3 +731,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index ab2230b3b..cdea0f5b0 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -178,6 +178,19 @@ type MapNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +273,23 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapDefaultValue returns the Default field value. func (a MapNestedAttribute) MapDefaultValue() defaults.Map { return a.Default @@ -284,6 +314,14 @@ func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.MapDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go index 1fd1a7624..94d5ff593 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -91,8 +91,6 @@ func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -236,8 +232,6 @@ func TestMapNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +270,6 @@ func TestMapNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +308,6 @@ func TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -354,8 +344,6 @@ func TestMapNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -400,8 +388,6 @@ func TestMapNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -440,8 +426,6 @@ func TestMapNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -480,8 +464,6 @@ func TestMapNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -520,8 +502,6 @@ func TestMapNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -560,8 +540,6 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -574,6 +552,38 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedDefaultValue(t *testing.T) { t.Parallel() @@ -621,8 +631,6 @@ func TestMapNestedAttributeMapNestedDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -661,8 +669,6 @@ func TestMapNestedAttributeMapNestedPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -701,8 +707,6 @@ func TestMapNestedAttributeMapNestedValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -909,11 +913,97 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -926,3 +1016,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/mapdefault/static_value_test.go b/resource/schema/mapdefault/static_value_test.go index 9b606d7c2..04d1a9325 100644 --- a/resource/schema/mapdefault/static_value_test.go +++ b/resource/schema/mapdefault/static_value_test.go @@ -41,8 +41,6 @@ func TestStaticValueDefaultMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/mapplanmodifier/requires_replace_if_configured_test.go b/resource/schema/mapplanmodifier/requires_replace_if_configured_test.go index b9b7bfd76..9eecb0eb9 100644 --- a/resource/schema/mapplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/mapplanmodifier/requires_replace_if_configured_test.go @@ -154,8 +154,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/mapplanmodifier/requires_replace_if_test.go b/resource/schema/mapplanmodifier/requires_replace_if_test.go index cfe362858..5edd5ceac 100644 --- a/resource/schema/mapplanmodifier/requires_replace_if_test.go +++ b/resource/schema/mapplanmodifier/requires_replace_if_test.go @@ -165,8 +165,6 @@ func TestRequiresReplaceIfModifierPlanModifyMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/mapplanmodifier/requires_replace_test.go b/resource/schema/mapplanmodifier/requires_replace_test.go index bb8b08b69..8d1f6f7eb 100644 --- a/resource/schema/mapplanmodifier/requires_replace_test.go +++ b/resource/schema/mapplanmodifier/requires_replace_test.go @@ -137,8 +137,6 @@ func TestRequiresReplaceModifierPlanModifyMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/mapplanmodifier/use_state_for_unknown_test.go b/resource/schema/mapplanmodifier/use_state_for_unknown_test.go index 3b4bf72eb..1ea2aa940 100644 --- a/resource/schema/mapplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/mapplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/nested_attribute_object_test.go b/resource/schema/nested_attribute_object_test.go index 7b325bbb8..f86c1952e 100644 --- a/resource/schema/nested_attribute_object_test.go +++ b/resource/schema/nested_attribute_object_test.go @@ -81,8 +81,6 @@ func TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +144,6 @@ func TestNestedAttributeObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -186,8 +182,6 @@ func TestNestedAttributeObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -224,8 +218,6 @@ func TestNestedAttributeObjectObjectPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +253,6 @@ func TestNestedAttributeObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -303,8 +293,6 @@ func TestNestedAttributeObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/nested_block_object_test.go b/resource/schema/nested_block_object_test.go index 95ff4613a..81e64c4e0 100644 --- a/resource/schema/nested_block_object_test.go +++ b/resource/schema/nested_block_object_test.go @@ -99,8 +99,6 @@ func TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -164,8 +162,6 @@ func TestNestedBlockObjectEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -204,8 +200,6 @@ func TestNestedBlockObjectGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +254,6 @@ func TestNestedBlockObjectGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -298,8 +290,6 @@ func TestNestedBlockObjectObjectPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -336,8 +326,6 @@ func TestNestedBlockObjectObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,8 +378,6 @@ func TestNestedBlockObjectType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index d2b9c59af..a443945e8 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -156,6 +156,16 @@ type NumberAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Number + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -218,6 +228,23 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a NumberAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberDefaultValue returns the Default field value. func (a NumberAttribute) NumberDefaultValue() defaults.Number { return a.Default diff --git a/resource/schema/number_attribute_test.go b/resource/schema/number_attribute_test.go index 2bc8731ca..027324224 100644 --- a/resource/schema/number_attribute_test.go +++ b/resource/schema/number_attribute_test.go @@ -63,8 +63,6 @@ func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -111,8 +109,6 @@ func TestNumberAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +142,6 @@ func TestNumberAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +174,6 @@ func TestNumberAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -214,8 +206,6 @@ func TestNumberAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +238,6 @@ func TestNumberAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -282,8 +270,6 @@ func TestNumberAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +302,6 @@ func TestNumberAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -350,8 +334,6 @@ func TestNumberAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -384,8 +366,6 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,6 +378,38 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.NumberAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberDefaultValue(t *testing.T) { t.Parallel() @@ -435,8 +447,6 @@ func TestNumberAttributeNumberDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -469,8 +479,6 @@ func TestNumberAttributeNumberPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -503,8 +511,6 @@ func TestNumberAttributeNumberValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -567,8 +573,6 @@ func TestNumberAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -581,3 +585,55 @@ func TestNumberAttributeValidateImplementation(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/numberdefault/static_value_test.go b/resource/schema/numberdefault/static_value_test.go index 48fa4e36a..a7c4ab79d 100644 --- a/resource/schema/numberdefault/static_value_test.go +++ b/resource/schema/numberdefault/static_value_test.go @@ -31,8 +31,6 @@ func TestStaticBigFloatDefaultNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/numberplanmodifier/requires_replace_if_configured_test.go b/resource/schema/numberplanmodifier/requires_replace_if_configured_test.go index f9280324b..88594020c 100644 --- a/resource/schema/numberplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/numberplanmodifier/requires_replace_if_configured_test.go @@ -152,8 +152,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/numberplanmodifier/requires_replace_if_test.go b/resource/schema/numberplanmodifier/requires_replace_if_test.go index ad12a05cd..67272731e 100644 --- a/resource/schema/numberplanmodifier/requires_replace_if_test.go +++ b/resource/schema/numberplanmodifier/requires_replace_if_test.go @@ -163,8 +163,6 @@ func TestRequiresReplaceIfModifierPlanModifyNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/numberplanmodifier/requires_replace_test.go b/resource/schema/numberplanmodifier/requires_replace_test.go index e88831f06..b902a544f 100644 --- a/resource/schema/numberplanmodifier/requires_replace_test.go +++ b/resource/schema/numberplanmodifier/requires_replace_test.go @@ -135,8 +135,6 @@ func TestRequiresReplaceModifierPlanModifyNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/numberplanmodifier/use_state_for_unknown_test.go b/resource/schema/numberplanmodifier/use_state_for_unknown_test.go index 982f17854..b910a4335 100644 --- a/resource/schema/numberplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/numberplanmodifier/use_state_for_unknown_test.go @@ -122,8 +122,6 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 7b9fe6a56..1de48e873 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -170,6 +170,16 @@ type ObjectAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into an @@ -234,6 +244,23 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ObjectAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectDefaultValue returns the Default field value. func (a ObjectAttribute) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go index 071e2293c..fd6df413b 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -69,8 +69,6 @@ func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -117,8 +115,6 @@ func TestObjectAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -157,8 +153,6 @@ func TestObjectAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -191,8 +185,6 @@ func TestObjectAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -225,8 +217,6 @@ func TestObjectAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -259,8 +249,6 @@ func TestObjectAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -293,8 +281,6 @@ func TestObjectAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -327,8 +313,6 @@ func TestObjectAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -361,8 +345,6 @@ func TestObjectAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +377,6 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -409,6 +389,38 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ObjectAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectDefaultValue(t *testing.T) { t.Parallel() @@ -460,8 +472,6 @@ func TestObjectAttributeObjectDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -494,8 +504,6 @@ func TestObjectAttributeObjectPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -528,8 +536,6 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -767,8 +773,6 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -781,3 +785,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/objectdefault/static_value_test.go b/resource/schema/objectdefault/static_value_test.go index c6f14f8e6..5aae03793 100644 --- a/resource/schema/objectdefault/static_value_test.go +++ b/resource/schema/objectdefault/static_value_test.go @@ -45,8 +45,6 @@ func TestStaticValueDefaultObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/objectplanmodifier/requires_replace_if_configured_test.go b/resource/schema/objectplanmodifier/requires_replace_if_configured_test.go index abc413c0d..df267a1f4 100644 --- a/resource/schema/objectplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/objectplanmodifier/requires_replace_if_configured_test.go @@ -154,8 +154,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/objectplanmodifier/requires_replace_if_test.go b/resource/schema/objectplanmodifier/requires_replace_if_test.go index 5723167a5..15417ca49 100644 --- a/resource/schema/objectplanmodifier/requires_replace_if_test.go +++ b/resource/schema/objectplanmodifier/requires_replace_if_test.go @@ -165,8 +165,6 @@ func TestRequiresReplaceIfModifierPlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/objectplanmodifier/requires_replace_test.go b/resource/schema/objectplanmodifier/requires_replace_test.go index af62eb615..4dd051d4a 100644 --- a/resource/schema/objectplanmodifier/requires_replace_test.go +++ b/resource/schema/objectplanmodifier/requires_replace_test.go @@ -137,8 +137,6 @@ func TestRequiresReplaceModifierPlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/objectplanmodifier/use_state_for_unknown_test.go b/resource/schema/objectplanmodifier/use_state_for_unknown_test.go index f285194a0..f90747529 100644 --- a/resource/schema/objectplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/objectplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/schema_test.go b/resource/schema/schema_test.go index 7df9790ac..a48fed812 100644 --- a/resource/schema/schema_test.go +++ b/resource/schema/schema_test.go @@ -100,8 +100,6 @@ func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -269,7 +267,6 @@ func TestSchemaAttributeAtPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -390,7 +387,6 @@ func TestSchemaAttributeAtTerraformPath(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -448,8 +444,6 @@ func TestSchemaGetAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -504,8 +498,6 @@ func TestSchemaGetBlocks(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -542,8 +534,6 @@ func TestSchemaGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -580,8 +570,6 @@ func TestSchemaGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -618,8 +606,6 @@ func TestSchemaGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -659,8 +645,6 @@ func TestSchemaGetVersion(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -707,8 +691,6 @@ func TestSchemaType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -848,8 +830,6 @@ func TestSchemaTypeAtPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -981,8 +961,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1038,8 +1016,6 @@ func TestSchemaValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1351,8 +1327,6 @@ func TestSchemaValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index 7a54221bb..2c9f08286 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -230,6 +230,23 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported for sets and set-based data. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetDefaultValue returns the Default field value. func (a SetAttribute) SetDefaultValue() defaults.Set { return a.Default diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go index 98483f724..268cb20b3 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -63,8 +63,6 @@ func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -111,8 +109,6 @@ func TestSetAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +147,6 @@ func TestSetAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +179,6 @@ func TestSetAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -219,8 +211,6 @@ func TestSetAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -253,8 +243,6 @@ func TestSetAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -287,8 +275,6 @@ func TestSetAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -321,8 +307,6 @@ func TestSetAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -355,8 +339,6 @@ func TestSetAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -389,8 +371,6 @@ func TestSetAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -403,6 +383,32 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetDefaultValue(t *testing.T) { t.Parallel() @@ -450,8 +456,6 @@ func TestSetAttributeSetDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -484,8 +488,6 @@ func TestSetAttributeSetPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -518,8 +520,6 @@ func TestSetAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -703,8 +703,6 @@ func TestSetAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -717,3 +715,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index dee37b591..c3b26780e 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -255,6 +255,23 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported for sets and set-based data. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetDefaultValue returns the Default field value. func (a SetNestedAttribute) SetDefaultValue() defaults.Set { return a.Default @@ -279,6 +296,10 @@ func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidSetNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.SetDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go index d148db457..406297a92 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -91,8 +91,6 @@ func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +143,6 @@ func TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -236,8 +232,6 @@ func TestSetNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,8 +270,6 @@ func TestSetNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -316,8 +308,6 @@ func TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -354,8 +344,6 @@ func TestSetNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -400,8 +388,6 @@ func TestSetNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -440,8 +426,6 @@ func TestSetNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -480,8 +464,6 @@ func TestSetNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -520,8 +502,6 @@ func TestSetNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -560,8 +540,6 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -574,6 +552,32 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetDefaultValue(t *testing.T) { t.Parallel() @@ -621,8 +625,6 @@ func TestSetNestedAttributeSetDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -661,8 +663,6 @@ func TestSetNestedAttributeSetPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -701,8 +701,6 @@ func TestSetNestedAttributeSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -909,11 +907,70 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "child-writeOnly-attribute-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a set nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a set nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, + "nested-attribute-with-child-writeOnly-attribute-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_nested_set": schema.SetNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a set nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a set nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -926,3 +983,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_nested_block.go b/resource/schema/set_nested_block.go index 78de8afb1..6b4b728bc 100644 --- a/resource/schema/set_nested_block.go +++ b/resource/schema/set_nested_block.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -15,7 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -223,6 +224,10 @@ func (b SetNestedBlock) Type() attr.Type { // errors or panics. This logic runs during the GetProviderSchema RPC and // should never include false positives. func (b SetNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if fwschema.BlockContainsAnyWriteOnlyChildAttributes(b) { + resp.Diagnostics.Append(fwschema.SetBlockCollectionWithWriteOnlyDiag(req.Path)) + } + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) } diff --git a/resource/schema/set_nested_block_test.go b/resource/schema/set_nested_block_test.go index 45d054366..409e5d0e5 100644 --- a/resource/schema/set_nested_block_test.go +++ b/resource/schema/set_nested_block_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -20,7 +22,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { @@ -87,8 +88,6 @@ func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -141,8 +140,6 @@ func TestSetNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -261,8 +258,6 @@ func TestSetNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +296,6 @@ func TestSetNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -341,8 +334,6 @@ func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -379,8 +370,6 @@ func TestSetNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -419,8 +408,6 @@ func TestSetNestedBlockSetPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +446,6 @@ func TestSetNestedBlockSetValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -517,8 +502,6 @@ func TestSetNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -566,11 +549,35 @@ func TestSetNestedBlockValidateImplementation(t *testing.T) { }, }, }, + "nestedobject-write-only": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a set nested block that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute within a set nested block must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/setdefault/static_value_test.go b/resource/schema/setdefault/static_value_test.go index 6778995b5..418bda46c 100644 --- a/resource/schema/setdefault/static_value_test.go +++ b/resource/schema/setdefault/static_value_test.go @@ -41,8 +41,6 @@ func TestStaticValueDefaultSet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/setplanmodifier/requires_replace_if_configured_test.go b/resource/schema/setplanmodifier/requires_replace_if_configured_test.go index 7a8f06f19..350f37aa4 100644 --- a/resource/schema/setplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/setplanmodifier/requires_replace_if_configured_test.go @@ -154,8 +154,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/setplanmodifier/requires_replace_if_test.go b/resource/schema/setplanmodifier/requires_replace_if_test.go index 24a0c3684..b5c057f8d 100644 --- a/resource/schema/setplanmodifier/requires_replace_if_test.go +++ b/resource/schema/setplanmodifier/requires_replace_if_test.go @@ -165,8 +165,6 @@ func TestRequiresReplaceIfModifierPlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/setplanmodifier/requires_replace_test.go b/resource/schema/setplanmodifier/requires_replace_test.go index 731b9a195..36a8a9671 100644 --- a/resource/schema/setplanmodifier/requires_replace_test.go +++ b/resource/schema/setplanmodifier/requires_replace_test.go @@ -137,8 +137,6 @@ func TestRequiresReplaceModifierPlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/setplanmodifier/use_state_for_unknown_test.go b/resource/schema/setplanmodifier/use_state_for_unknown_test.go index 47a324dac..c55bc2034 100644 --- a/resource/schema/setplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/setplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 3dbda942a..0d4e00e24 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -167,6 +167,16 @@ type SingleNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -271,6 +281,23 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectDefaultValue returns the Default field value. func (a SingleNestedAttribute) ObjectDefaultValue() defaults.Object { return a.Default @@ -295,6 +322,14 @@ func (a SingleNestedAttribute) ValidateImplementation(ctx context.Context, req f resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ObjectDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go index 1f20b65b3..259102bfc 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -89,8 +89,6 @@ func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -180,8 +178,6 @@ func TestSingleNestedAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -218,8 +214,6 @@ func TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -256,8 +250,6 @@ func TestSingleNestedAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,8 +286,6 @@ func TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -330,8 +320,6 @@ func TestSingleNestedAttributeGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -372,8 +360,6 @@ func TestSingleNestedAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -410,8 +396,6 @@ func TestSingleNestedAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -448,8 +432,6 @@ func TestSingleNestedAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -486,8 +468,6 @@ func TestSingleNestedAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -524,8 +504,6 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -538,6 +516,38 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectDefaultValue(t *testing.T) { t.Parallel() @@ -589,8 +599,6 @@ func TestSingleNestedAttributeObjectDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -627,8 +635,6 @@ func TestSingleNestedAttributeObjectPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -665,8 +671,6 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -819,11 +823,89 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -836,3 +918,55 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/single_nested_block_test.go b/resource/schema/single_nested_block_test.go index 8216ca180..5c2c7a0c5 100644 --- a/resource/schema/single_nested_block_test.go +++ b/resource/schema/single_nested_block_test.go @@ -100,8 +100,6 @@ func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -152,8 +150,6 @@ func TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -254,8 +250,6 @@ func TestSingleNestedBlockEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -292,8 +286,6 @@ func TestSingleNestedBlockGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -330,8 +322,6 @@ func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -380,8 +370,6 @@ func TestSingleNestedBlockGetNestedObject(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,8 +406,6 @@ func TestSingleNestedBlockObjectPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -456,8 +442,6 @@ func TestSingleNestedBlockObjectValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -510,8 +494,6 @@ func TestSingleNestedBlockType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 7e3b8a1c2..12340ee6e 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -152,6 +152,16 @@ type StringAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.String + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -214,6 +224,23 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a StringAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringDefaultValue returns the Default field value. func (a StringAttribute) StringDefaultValue() defaults.String { return a.Default diff --git a/resource/schema/string_attribute_test.go b/resource/schema/string_attribute_test.go index 65bc15b10..c1dffa2f2 100644 --- a/resource/schema/string_attribute_test.go +++ b/resource/schema/string_attribute_test.go @@ -62,8 +62,6 @@ func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -110,8 +108,6 @@ func TestStringAttributeGetDeprecationMessage(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -145,8 +141,6 @@ func TestStringAttributeEqual(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -179,8 +173,6 @@ func TestStringAttributeGetDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -213,8 +205,6 @@ func TestStringAttributeGetMarkdownDescription(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -247,8 +237,6 @@ func TestStringAttributeGetType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -281,8 +269,6 @@ func TestStringAttributeIsComputed(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -315,8 +301,6 @@ func TestStringAttributeIsOptional(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +333,6 @@ func TestStringAttributeIsRequired(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -383,8 +365,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -397,6 +377,38 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.StringAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringDefaultValue(t *testing.T) { t.Parallel() @@ -430,8 +442,6 @@ func TestStringAttributeStringDefaultValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -464,8 +474,6 @@ func TestStringAttributeStringPlanModifiers(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -498,8 +506,6 @@ func TestStringAttributeStringValidators(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -562,8 +568,6 @@ func TestStringAttributeValidateImplementation(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -576,3 +580,55 @@ func TestStringAttributeValidateImplementation(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/stringdefault/static_value_test.go b/resource/schema/stringdefault/static_value_test.go index 96b0a786e..6fb04b7fc 100644 --- a/resource/schema/stringdefault/static_value_test.go +++ b/resource/schema/stringdefault/static_value_test.go @@ -30,8 +30,6 @@ func TestStaticStringDefaultString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/stringplanmodifier/requires_replace_if_configured_test.go b/resource/schema/stringplanmodifier/requires_replace_if_configured_test.go index 2a2bb1988..e9c2dee01 100644 --- a/resource/schema/stringplanmodifier/requires_replace_if_configured_test.go +++ b/resource/schema/stringplanmodifier/requires_replace_if_configured_test.go @@ -151,8 +151,6 @@ func TestRequiresReplaceIfConfiguredModifierPlanModifyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/stringplanmodifier/requires_replace_if_test.go b/resource/schema/stringplanmodifier/requires_replace_if_test.go index 997949b5d..5473f6d75 100644 --- a/resource/schema/stringplanmodifier/requires_replace_if_test.go +++ b/resource/schema/stringplanmodifier/requires_replace_if_test.go @@ -162,8 +162,6 @@ func TestRequiresReplaceIfModifierPlanModifyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/stringplanmodifier/requires_replace_test.go b/resource/schema/stringplanmodifier/requires_replace_test.go index 7e3fe5659..1ec274e28 100644 --- a/resource/schema/stringplanmodifier/requires_replace_test.go +++ b/resource/schema/stringplanmodifier/requires_replace_test.go @@ -134,8 +134,6 @@ func TestRequiresReplaceModifierPlanModifyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/stringplanmodifier/use_state_for_unknown_test.go b/resource/schema/stringplanmodifier/use_state_for_unknown_test.go index 0bdd1f513..6a686469c 100644 --- a/resource/schema/stringplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/stringplanmodifier/use_state_for_unknown_test.go @@ -121,8 +121,6 @@ func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/resource/schema/write_only_nested_attribute_validation_test.go b/resource/schema/write_only_nested_attribute_validation_test.go new file mode 100644 index 000000000..b14b97931 --- /dev/null +++ b/resource/schema/write_only_nested_attribute_validation_test.go @@ -0,0 +1,1801 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() + tests := map[string]struct { + nestedAttr schema.NestedAttribute + expected bool + }{ + "empty nested attribute returns true": { + nestedAttr: schema.ListNestedAttribute{}, + expected: true, + }, + "list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + expected: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := fwschema.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() + tests := map[string]struct { + nestedAttr schema.NestedAttribute + expected bool + }{ + "empty nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{}, + expected: false, + }, + "list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + Optional: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with no writeOnly child nested attributes returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with one writeOnly nested child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + + "single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: true, + }, + "single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := fwschema.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestBlockContainsAnyWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() + tests := map[string]struct { + block schema.Block + expected bool + }{ + "empty nested block returns false": { + block: schema.ListNestedBlock{}, + expected: false, + }, + "list nested block with writeOnly child attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested block with non-writeOnly child attribute returns false": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested block with multiple writeOnly child attributes returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested block with one non-writeOnly child attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "list nested block with writeOnly child nested attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested block with non-writeOnly child nested attribute returns false": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested block with one non-writeOnly child nested attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested block with one non-writeOnly nested child attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list double-nested block with top-level writeOnly nested child attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list double-nested block with one non-writeOnly nested child attribute returns true": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested block with non-writeOnly child attribute returns false": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested block with multiple writeOnly child attributes returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested block with one non-writeOnly child attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "set nested block with writeOnly child nested attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested block with non-writeOnly child nested attribute returns false": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested block with one non-writeOnly child nested attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested block with one non-writeOnly nested child attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set double-nested block with top-level writeOnly nested child attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_set_nested_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set double-nested block with one non-writeOnly nested child attribute returns true": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_set_nested_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested block with non-writeOnly child attribute returns false": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested block with multiple writeOnly child attributes returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested block with one non-writeOnly child attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: true, + }, + "single nested block with writeOnly child nested attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested block with non-writeOnly child nested attribute returns false": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested block with one non-writeOnly child nested attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested block with one non-writeOnly nested child attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single double-nested block with top-level writeOnly nested child attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single double-nested block with one non-writeOnly nested child attribute returns true": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "double_single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := fwschema.BlockContainsAnyWriteOnlyChildAttributes(tt.block); got != tt.expected { + t.Errorf("BlockContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/resource/update.go b/resource/update.go index 0dceaf8ab..d832b24c3 100644 --- a/resource/update.go +++ b/resource/update.go @@ -27,6 +27,10 @@ type UpdateRequest struct { // operation. State tfsdk.State + // Identity is the planned identity for the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config @@ -49,6 +53,14 @@ type UpdateResponse struct { // should be set during the resource's Update operation. State tfsdk.State + // Identity is the identity of the resource following the Update operation. + // This field is pre-populated from UpdateRequest.Identity and + // should be set during the resource's Update operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Update operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Update operation. // This field is pre-populated from UpdateRequest.Private and // can be modified during the resource's Update operation. diff --git a/resource/validate_config.go b/resource/validate_config.go index 40e1213bf..f35c4aa3f 100644 --- a/resource/validate_config.go +++ b/resource/validate_config.go @@ -8,6 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ValidateConfigClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the +// ValidateResourceConfig RPC, such as forward-compatible Terraform behavior +// changes. +type ValidateConfigClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + WriteOnlyAttributesAllowed bool +} + // ValidateConfigRequest represents a request to validate the // configuration of a resource. An instance of this request struct is // supplied as an argument to the Resource ValidateConfig receiver method @@ -19,6 +30,11 @@ type ValidateConfigRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // the ValidateResourceConfig RPC, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateConfigClientCapabilities } // ValidateConfigResponse represents a response to a diff --git a/schema/validator/bool.go b/schema/validator/bool.go index 64115e713..26f6df1a4 100644 --- a/schema/validator/bool.go +++ b/schema/validator/bool.go @@ -35,6 +35,11 @@ type BoolRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Bool + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // BoolResponse is a response to a BoolRequest. diff --git a/schema/validator/client_capabilities.go b/schema/validator/client_capabilities.go new file mode 100644 index 000000000..8f0bbe9a0 --- /dev/null +++ b/schema/validator/client_capabilities.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +// ValidateSchemaClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the schema validation +// RPCs, such as forward-compatible Terraform behavior changes. +type ValidateSchemaClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + // + // This client capability is only populated during managed resource schema + // validation. + WriteOnlyAttributesAllowed bool +} diff --git a/schema/validator/dynamic.go b/schema/validator/dynamic.go index b035175a1..e5d2cb58c 100644 --- a/schema/validator/dynamic.go +++ b/schema/validator/dynamic.go @@ -35,6 +35,11 @@ type DynamicRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Dynamic + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // DynamicResponse is a response to a DynamicRequest. diff --git a/schema/validator/float32.go b/schema/validator/float32.go index c1cd8421d..9f38507b2 100644 --- a/schema/validator/float32.go +++ b/schema/validator/float32.go @@ -35,6 +35,11 @@ type Float32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float32Response is a response to a Float32Request. diff --git a/schema/validator/float64.go b/schema/validator/float64.go index f09111ac7..7c788d8f3 100644 --- a/schema/validator/float64.go +++ b/schema/validator/float64.go @@ -35,6 +35,11 @@ type Float64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float64Response is a response to a Float64Request. diff --git a/schema/validator/int32.go b/schema/validator/int32.go index 2cbbc3cc2..d13185226 100644 --- a/schema/validator/int32.go +++ b/schema/validator/int32.go @@ -35,6 +35,11 @@ type Int32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int32Response is a response to a Int32Request. diff --git a/schema/validator/int64.go b/schema/validator/int64.go index 8e8accdcb..061ab1cf8 100644 --- a/schema/validator/int64.go +++ b/schema/validator/int64.go @@ -35,6 +35,11 @@ type Int64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int64Response is a response to a Int64Request. diff --git a/schema/validator/list.go b/schema/validator/list.go index e5b6083d8..e2dc5ecd1 100644 --- a/schema/validator/list.go +++ b/schema/validator/list.go @@ -35,6 +35,11 @@ type ListRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.List + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ListResponse is a response to a ListRequest. diff --git a/schema/validator/map.go b/schema/validator/map.go index 2a41cc7ac..eda09a239 100644 --- a/schema/validator/map.go +++ b/schema/validator/map.go @@ -35,6 +35,11 @@ type MapRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Map + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // MapResponse is a response to a MapRequest. diff --git a/schema/validator/number.go b/schema/validator/number.go index ef7692c20..2bccf9ac6 100644 --- a/schema/validator/number.go +++ b/schema/validator/number.go @@ -35,6 +35,11 @@ type NumberRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Number + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // NumberResponse is a response to a NumberRequest. diff --git a/schema/validator/object.go b/schema/validator/object.go index 88029e0ad..a2d96a24c 100644 --- a/schema/validator/object.go +++ b/schema/validator/object.go @@ -35,6 +35,11 @@ type ObjectRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Object + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ObjectResponse is a response to a ObjectRequest. diff --git a/schema/validator/set.go b/schema/validator/set.go index ce7cdea34..f3aaf0b0f 100644 --- a/schema/validator/set.go +++ b/schema/validator/set.go @@ -35,6 +35,11 @@ type SetRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Set + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // SetResponse is a response to a SetRequest. diff --git a/schema/validator/string.go b/schema/validator/string.go index b453a7bfc..4427e2691 100644 --- a/schema/validator/string.go +++ b/schema/validator/string.go @@ -35,6 +35,11 @@ type StringRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.String + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // StringResponse is a response to a StringRequest. diff --git a/tfsdk/config_test.go b/tfsdk/config_test.go index 10955c5b5..313e98232 100644 --- a/tfsdk/config_test.go +++ b/tfsdk/config_test.go @@ -105,8 +105,6 @@ func TestConfigGet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -183,7 +181,6 @@ func TestConfigGetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -272,8 +269,6 @@ func TestConfigPathMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/convert_test.go b/tfsdk/convert_test.go index ee3a6b27b..4bc4efb2b 100644 --- a/tfsdk/convert_test.go +++ b/tfsdk/convert_test.go @@ -52,7 +52,6 @@ func TestConvert(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/ephemeral_result_data_test.go b/tfsdk/ephemeral_result_data_test.go index d1cbc5a1a..4e2cbb4da 100644 --- a/tfsdk/ephemeral_result_data_test.go +++ b/tfsdk/ephemeral_result_data_test.go @@ -105,8 +105,6 @@ func TestEphemeralResultDataGet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -183,7 +181,6 @@ func TestEphemeralResultDataGetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -272,8 +269,6 @@ func TestEphemeralResultDataPathMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,7 +359,6 @@ func TestEphemeralResultDataSet(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -466,7 +460,6 @@ func TestEphemeralResultDataSetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index d256d33dd..a430b3bf7 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -105,8 +105,6 @@ func TestPlanGet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -183,7 +181,6 @@ func TestPlanGetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -272,8 +269,6 @@ func TestPlanPathMatches(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -364,7 +359,6 @@ func TestPlanSet(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -466,7 +460,6 @@ func TestPlanSetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/resource_identity.go b/tfsdk/resource_identity.go new file mode 100644 index 000000000..fd9f30579 --- /dev/null +++ b/tfsdk/resource_identity.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ResourceIdentity represents the identity data for a managed resource. +type ResourceIdentity struct { + Raw tftypes.Value + Schema fwschema.Schema +} + +// Get populates the struct passed as `target` with the entire identity. +func (s ResourceIdentity) Get(ctx context.Context, target interface{}) diag.Diagnostics { + return s.data().Get(ctx, target) +} + +// GetAttribute retrieves the attribute found at `path` and populates +// the `target` with the value. +// +// Elements under null or unknown collections return null values, however this +// behavior is not protected by compatibility promises. +func (s ResourceIdentity) GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics { + return s.data().GetAtPath(ctx, path, target) +} + +// PathMatches returns all matching path.Paths from the given path.Expression. +// +// If a parent path is null or unknown, which would prevent a full expression +// from matching, the parent path is returned rather than no match to prevent +// false positives. +func (s ResourceIdentity) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return s.data().PathMatches(ctx, pathExpr) +} + +// Set populates the entire identity using the supplied Go value. The value `val` +// should be a struct whose values have one of the attr.Value types. Each field +// must be tagged with the corresponding schema field. +func (s *ResourceIdentity) Set(ctx context.Context, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.Set(ctx, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +// SetAttribute sets the attribute at `path` using the supplied Go value. +// +// The attribute path and value must be valid with the current schema. If the +// attribute path already has a value, it will be overwritten. If the attribute +// path does not have a value, it will be added. +// +// The value must not be an untyped nil. Use a typed nil or types package null +// value function instead. For example with a types.StringType attribute, +// use (*string)(nil) or types.StringNull(). +// +// Lists can only have the next element added according to the current length. +func (s *ResourceIdentity) SetAttribute(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics { + // If s is nil, then calling s.data triggers a nil pointer error so we return the error diag here + if s == nil { + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Identity Definition", + "An unexpected error was encountered when attempting to set a resource identity attribute. The resource does not indicate support via a resource identity schema.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer."), + } + } + + data := s.data() + diags := data.SetAtPath(ctx, path, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +func (s ResourceIdentity) data() *fwschemadata.Data { + return &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: s.Schema, + TerraformValue: s.Raw, + } +} diff --git a/tfsdk/resource_identity_test.go b/tfsdk/resource_identity_test.go new file mode 100644 index 000000000..e73d07872 --- /dev/null +++ b/tfsdk/resource_identity_test.go @@ -0,0 +1,482 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk_test + +import ( + "context" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + intreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentityGet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + resourceIdentity tfsdk.ResourceIdentity + target any + expected any + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataGet for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, "test"), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"string"` + }), + expected: &struct { + String types.String `tfsdk:"string"` + }{ + String: types.StringValue("test"), + }, + }, + "diagnostic": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "bool": tftypes.NewValue(tftypes.Bool, nil), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "bool": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"bool"` + }), + expected: &struct { + String types.String `tfsdk:"bool"` + }{ + String: types.String{}, + }, + expectedDiags: diag.Diagnostics{ + diag.WithPath( + path.Root("bool"), + intreflect.DiagNewAttributeValueIntoWrongType{ + ValType: reflect.TypeOf(types.Bool{}), + TargetType: reflect.TypeOf(types.String{}), + SchemaType: types.BoolType, + }, + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.resourceIdentity.Get(context.Background(), testCase.target) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(testCase.target, testCase.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentityGetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + target interface{} + expected interface{} + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataGetAtPath for more exhaustive unit + // testing. These test cases are to ensure ResourceIdentity schema and data values + // are passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + }, + target: new(string), + expected: pointer("namevalue"), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + target: new(testtypes.String), + expected: &testtypes.String{InternalString: types.StringValue("namevalue"), CreatedBy: testtypes.StringTypeWithValidateWarning{}}, + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.GetAttribute(context.Background(), path.Root("name"), tc.target) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.target, tc.expected, cmp.Transformer("testtypes", func(in *testtypes.String) testtypes.String { return *in }), cmp.Transformer("types", func(in *types.String) types.String { return *in })); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentityPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + resourceIdentity tfsdk.ResourceIdentity + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataPathMatches for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + resourceIdentity: tfsdk.ResourceIdentity{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + resourceIdentity: tfsdk.ResourceIdentity{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Path Expression for Schema", + "The Terraform Provider unexpectedly provided a path expression that does not match the current schema. "+ + "This can happen if the path expression does not correctly follow the schema in structure or types. "+ + "Please report this to the provider developers.\n\n"+ + "Path Expression: not-test", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.resourceIdentity.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestResourceIdentitySet(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSet for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "oldvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.Value{}, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.Set(context.Background(), tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.resourceIdentity.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentitySetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + path path.Path + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSetAtPath for more exhaustive unit + // testing. These test cases are to ensure ResourceIdentity schema and data values + // are passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "other": testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + }, + }, + }, + path: path.Root("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "originalname"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + path: path.Root("name"), + val: "newname", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newname"), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(path.Root("name")), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.SetAttribute(context.Background(), tc.path, tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + for _, diagnostic := range diags { + t.Log(diagnostic) + } + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.resourceIdentity.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index ced1e21f8..2d9e30ea0 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -105,8 +105,6 @@ func TestStateGet(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -183,7 +181,6 @@ func TestStateGetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,7 +271,6 @@ func TestStateSet(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() @@ -376,7 +372,6 @@ func TestStateSetAttribute(t *testing.T) { } for name, tc := range testCases { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/value_as_test.go b/tfsdk/value_as_test.go index a5e0cf80c..d7eba4184 100644 --- a/tfsdk/value_as_test.go +++ b/tfsdk/value_as_test.go @@ -377,7 +377,6 @@ func TestValueAs(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tfsdk/value_from_test.go b/tfsdk/value_from_test.go index 5fe219e30..27bff23f0 100644 --- a/tfsdk/value_from_test.go +++ b/tfsdk/value_from_test.go @@ -199,7 +199,6 @@ func TestValueFrom(t *testing.T) { } for name, tc := range tests { - name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/tools/go.mod b/tools/go.mod index 3c9751431..66628b325 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,24 +1,23 @@ module tools -go 1.22.7 +go 1.23.7 -require github.com/hashicorp/copywrite v0.19.0 +require github.com/hashicorp/copywrite v0.22.0 require ( - github.com/AlecAivazis/survey/v2 v2.3.6 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 // indirect - github.com/cli/go-gh v1.2.1 // indirect + github.com/cli/go-gh/v2 v2.11.2 // indirect github.com/cli/safeexec v1.0.0 // indirect - github.com/cli/shurcooL-graphql v0.0.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/fatih/color v1.13.0 // indirect 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.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-github/v45 v45.2.0 // indirect @@ -26,41 +25,37 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jedib0t/go-pretty/v6 v6.4.6 // indirect github.com/joho/godotenv v1.3.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/knadh/koanf v1.5.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mergestat/timediff v0.0.3 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/muesli/termenv v0.12.0 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/samber/lo v1.37.0 // indirect github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/thanhpk/randstr v1.0.4 // indirect - github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect - golang.org/x/crypto v0.31.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.23.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/tools/go.sum b/tools/go.sum index aae7cc64e..1b0fbabba 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= -github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= @@ -44,12 +44,10 @@ github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o= -github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM= +github.com/cli/go-gh/v2 v2.11.2 h1:oad1+sESTPNTiTvh3I3t8UmxuovNDxhwLzeMHk45Q9w= +github.com/cli/go-gh/v2 v2.11.2/go.mod h1:vVFhi3TfjseIW26ED9itAR8gQK0aVThTm8sYrsZ5QTI= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/cli/shurcooL-graphql v0.0.2 h1:rwP5/qQQ2fM0TzkUTwtt6E2LbIYf6R+39cUXTa04NYk= -github.com/cli/shurcooL-graphql v0.0.2/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= @@ -98,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= @@ -146,12 +144,10 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= -github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 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.19.0 h1:f9LVxTDBfFYeQmdBpOsZ+HWknXonI8ZwubbO/RwyuCo= -github.com/hashicorp/copywrite v0.19.0/go.mod h1:6wvQH+ICDoD2bpjO1RJ6fi+h3aY5NeLEM12oTkEtFoc= +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= @@ -186,8 +182,6 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= -github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= @@ -226,8 +220,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -243,10 +235,11 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= @@ -278,10 +271,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -319,8 +308,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -357,8 +347,6 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/thanhpk/randstr v1.0.4 h1:IN78qu/bR+My+gHCvMEXhR/i5oriVHcTB/BJJIRTsNo= github.com/thanhpk/randstr v1.0.4/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= -github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= -github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -386,8 +374,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= @@ -421,13 +409,12 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +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= @@ -444,8 +431,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -479,16 +466,12 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -496,17 +479,16 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.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-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -518,8 +500,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +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/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -578,8 +560,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= -gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/types/basetypes/bool_type_test.go b/types/basetypes/bool_type_test.go index 8d55d8309..2280dcfa3 100644 --- a/types/basetypes/bool_type_test.go +++ b/types/basetypes/bool_type_test.go @@ -42,7 +42,6 @@ func TestBoolTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/bool_value_test.go b/types/basetypes/bool_value_test.go index 001006891..b106a15f8 100644 --- a/types/basetypes/bool_value_test.go +++ b/types/basetypes/bool_value_test.go @@ -38,7 +38,6 @@ func TestBoolValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -156,7 +155,6 @@ func TestBoolValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -190,8 +188,6 @@ func TestBoolValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -226,8 +222,6 @@ func TestBoolValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -267,7 +261,6 @@ func TestBoolValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -305,8 +298,6 @@ func TestBoolValueValueBool(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -345,8 +336,6 @@ func TestBoolValueValueBoolPointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -377,8 +366,6 @@ func TestNewBoolPointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/dynamic_type_test.go b/types/basetypes/dynamic_type_test.go index 279a7d3d3..65fe89e34 100644 --- a/types/basetypes/dynamic_type_test.go +++ b/types/basetypes/dynamic_type_test.go @@ -38,7 +38,6 @@ func TestDynamicTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -402,7 +401,6 @@ func TestDynamicTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/dynamic_value_test.go b/types/basetypes/dynamic_value_test.go index ba8512d17..df0cb039d 100644 --- a/types/basetypes/dynamic_value_test.go +++ b/types/basetypes/dynamic_value_test.go @@ -43,8 +43,6 @@ func TestNewDynamicValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -158,7 +156,6 @@ func TestDynamicValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -385,7 +382,6 @@ func TestDynamicValueEqual(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -467,7 +463,6 @@ func TestDynamicValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -622,7 +617,6 @@ func TestDynamicValueIsUnderlyingValueNull(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -777,7 +771,6 @@ func TestDynamicValueIsUnderlyingValueUnknown(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/float32_type_test.go b/types/basetypes/float32_type_test.go index a409b35c5..49ff4960d 100644 --- a/types/basetypes/float32_type_test.go +++ b/types/basetypes/float32_type_test.go @@ -101,7 +101,6 @@ func TestFloat32TypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/float32_value_test.go b/types/basetypes/float32_value_test.go index 73781613f..9d6cc7fa5 100644 --- a/types/basetypes/float32_value_test.go +++ b/types/basetypes/float32_value_test.go @@ -60,7 +60,6 @@ func TestFloat32ValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -214,7 +213,6 @@ func TestFloat32ValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,8 +246,6 @@ func TestFloat32ValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -284,8 +280,6 @@ func TestFloat32ValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -352,7 +346,6 @@ func TestFloat32ValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -386,8 +379,6 @@ func TestFloat32ValueValueFloat32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -422,8 +413,6 @@ func TestFloat32ValueValueFloat32Pointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -454,8 +443,6 @@ func TestNewFloat32PointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -527,7 +514,6 @@ func TestFloat32ValueFloat32SemanticEquals(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/float64_type_test.go b/types/basetypes/float64_type_test.go index 4f1035254..2e86204d9 100644 --- a/types/basetypes/float64_type_test.go +++ b/types/basetypes/float64_type_test.go @@ -92,8 +92,6 @@ func TestFloat64TypeValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -189,7 +187,6 @@ func TestFloat64TypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/float64_value_test.go b/types/basetypes/float64_value_test.go index ca9dca6b9..37b5b6c55 100644 --- a/types/basetypes/float64_value_test.go +++ b/types/basetypes/float64_value_test.go @@ -72,7 +72,6 @@ func TestFloat64ValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -226,7 +225,6 @@ func TestFloat64ValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,8 +258,6 @@ func TestFloat64ValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -296,8 +292,6 @@ func TestFloat64ValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -357,7 +351,6 @@ func TestFloat64ValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -391,8 +384,6 @@ func TestFloat64ValueValueFloat64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -427,8 +418,6 @@ func TestFloat64ValueValueFloat64Pointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -459,8 +448,6 @@ func TestNewFloat64PointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -532,7 +519,6 @@ func TestFloat64ValueFloat64SemanticEquals(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/int32_type_test.go b/types/basetypes/int32_type_test.go index af5c8bdd0..f47ddd803 100644 --- a/types/basetypes/int32_type_test.go +++ b/types/basetypes/int32_type_test.go @@ -60,7 +60,6 @@ func TestInt32TypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/int32_value_test.go b/types/basetypes/int32_value_test.go index 15f505cac..89a366a64 100644 --- a/types/basetypes/int32_value_test.go +++ b/types/basetypes/int32_value_test.go @@ -37,7 +37,6 @@ func TestInt32ValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -115,7 +114,6 @@ func TestInt32ValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -149,8 +147,6 @@ func TestInt32ValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -185,8 +181,6 @@ func TestInt32ValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -238,7 +232,6 @@ func TestInt32ValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -272,8 +265,6 @@ func TestInt32ValueValueInt32(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -308,8 +299,6 @@ func TestInt32ValueValueInt32Pointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -340,8 +329,6 @@ func TestNewInt32PointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/int64_type_test.go b/types/basetypes/int64_type_test.go index f99866052..6eb115bc9 100644 --- a/types/basetypes/int64_type_test.go +++ b/types/basetypes/int64_type_test.go @@ -38,7 +38,6 @@ func TestInt64TypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/int64_value_test.go b/types/basetypes/int64_value_test.go index 8afbcc18a..ba2de237c 100644 --- a/types/basetypes/int64_value_test.go +++ b/types/basetypes/int64_value_test.go @@ -36,7 +36,6 @@ func TestInt64ValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -114,7 +113,6 @@ func TestInt64ValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -148,8 +146,6 @@ func TestInt64ValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -184,8 +180,6 @@ func TestInt64ValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -237,7 +231,6 @@ func TestInt64ValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -271,8 +264,6 @@ func TestInt64ValueValueInt64(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -307,8 +298,6 @@ func TestInt64ValueValueInt64Pointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -339,8 +328,6 @@ func TestNewInt64PointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/list_type_test.go b/types/basetypes/list_type_test.go index 6939a8f4b..9ecd65b82 100644 --- a/types/basetypes/list_type_test.go +++ b/types/basetypes/list_type_test.go @@ -30,8 +30,6 @@ func TestListTypeElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,7 +94,6 @@ func TestListTypeTerraformType(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -229,7 +226,6 @@ func TestListTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -299,7 +295,6 @@ func TestListTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -329,8 +324,6 @@ func TestListTypeString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/list_value_test.go b/types/basetypes/list_value_test.go index e63eb8088..2532d4bd4 100644 --- a/types/basetypes/list_value_test.go +++ b/types/basetypes/list_value_test.go @@ -66,8 +66,6 @@ func TestNewListValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -184,8 +182,6 @@ func TestNewListValueFrom(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -343,7 +339,6 @@ func TestListValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,8 +390,6 @@ func TestListValueElements(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -442,8 +435,6 @@ func TestListValueElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -635,7 +626,6 @@ func TestListValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -669,8 +659,6 @@ func TestListValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -705,8 +693,6 @@ func TestListValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -776,7 +762,6 @@ func TestListValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -845,7 +830,6 @@ func TestListValueType(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -899,8 +883,6 @@ func TestListTypeValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/map_type_test.go b/types/basetypes/map_type_test.go index f3bc1bd9f..d13edb259 100644 --- a/types/basetypes/map_type_test.go +++ b/types/basetypes/map_type_test.go @@ -31,8 +31,6 @@ func TestMapTypeElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -97,7 +95,6 @@ func TestMapTypeTerraformType(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -186,7 +183,6 @@ func TestMapTypeValueFromTerraform(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -276,7 +272,6 @@ func TestMapTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -306,8 +301,6 @@ func TestMapTypeString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/map_value_test.go b/types/basetypes/map_value_test.go index 0debecd9f..293fff102 100644 --- a/types/basetypes/map_value_test.go +++ b/types/basetypes/map_value_test.go @@ -66,8 +66,6 @@ func TestNewMapValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -184,8 +182,6 @@ func TestNewMapValueFrom(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -346,7 +342,6 @@ func TestMapValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,8 +393,6 @@ func TestMapValueElements(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -445,8 +438,6 @@ func TestMapValueElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -638,7 +629,6 @@ func TestMapValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -672,8 +662,6 @@ func TestMapValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -708,8 +696,6 @@ func TestMapValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -791,7 +777,6 @@ func TestMapValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -860,7 +845,6 @@ func TestMapValueType(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -914,8 +898,6 @@ func TestMapTypeValidate(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/number_type_test.go b/types/basetypes/number_type_test.go index 287e76035..153e8f9c2 100644 --- a/types/basetypes/number_type_test.go +++ b/types/basetypes/number_type_test.go @@ -39,7 +39,6 @@ func TestNumberTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/number_value_test.go b/types/basetypes/number_value_test.go index 27a25ac7d..08ca0c2bc 100644 --- a/types/basetypes/number_value_test.go +++ b/types/basetypes/number_value_test.go @@ -44,7 +44,6 @@ func TestNumberValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -167,7 +166,6 @@ func TestNumberValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -201,8 +199,6 @@ func TestNumberValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -237,8 +233,6 @@ func TestNumberValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -294,7 +288,6 @@ func TestNumberValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -332,8 +325,6 @@ func TestNumberValueValueBigFloat(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/object_type_test.go b/types/basetypes/object_type_test.go index 59d1ea838..0e1407578 100644 --- a/types/basetypes/object_type_test.go +++ b/types/basetypes/object_type_test.go @@ -220,7 +220,6 @@ func TestObjectTypeValueFromTerraform(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,7 +397,6 @@ func TestObjectTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -432,8 +430,6 @@ func TestObjectTypeString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/object_value_test.go b/types/basetypes/object_value_test.go index 73440f29c..4ccb41a41 100644 --- a/types/basetypes/object_value_test.go +++ b/types/basetypes/object_value_test.go @@ -153,8 +153,6 @@ func TestNewObjectValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -313,8 +311,6 @@ func TestNewObjectValueFrom(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -620,8 +616,6 @@ func TestObjectValueAttributes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -677,8 +671,6 @@ func TestObjectValueAttributeTypes(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1214,7 +1206,6 @@ func TestObjectValueToTerraformValue(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -1538,7 +1529,6 @@ func TestObjectValueEqual(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -1575,8 +1565,6 @@ func TestObjectValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1614,8 +1602,6 @@ func TestObjectValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1723,7 +1709,6 @@ func TestObjectValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -1828,7 +1813,6 @@ func TestObjectValueType(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/set_type_test.go b/types/basetypes/set_type_test.go index 4e645aa18..30b31222f 100644 --- a/types/basetypes/set_type_test.go +++ b/types/basetypes/set_type_test.go @@ -30,8 +30,6 @@ func TestSetTypeElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -96,7 +94,6 @@ func TestSetTypeTerraformType(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -249,7 +246,6 @@ func TestSetTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -319,7 +315,6 @@ func TestSetTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +344,6 @@ func TestSetTypeString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/set_value_test.go b/types/basetypes/set_value_test.go index 260382d0b..5dbfa25a7 100644 --- a/types/basetypes/set_value_test.go +++ b/types/basetypes/set_value_test.go @@ -285,7 +285,6 @@ func TestSetTypeValidate(t *testing.T) { }, } for name, testCase := range testCases { - name, testCase := name, testCase t.Run(name, func(t *testing.T) { t.Parallel() @@ -349,8 +348,6 @@ func TestNewSetValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -467,8 +464,6 @@ func TestNewSetValueFrom(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -596,7 +591,6 @@ func TestSetValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -648,8 +642,6 @@ func TestSetValueElements(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -695,8 +687,6 @@ func TestSetValueElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -888,7 +878,6 @@ func TestSetValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -922,8 +911,6 @@ func TestSetValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -958,8 +945,6 @@ func TestSetValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -1029,7 +1014,6 @@ func TestSetValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -1098,7 +1082,6 @@ func TestSetValueType(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/string_type_test.go b/types/basetypes/string_type_test.go index 097bb89c1..bcaf9e6d1 100644 --- a/types/basetypes/string_type_test.go +++ b/types/basetypes/string_type_test.go @@ -38,7 +38,6 @@ func TestStringTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/types/basetypes/string_value_test.go b/types/basetypes/string_value_test.go index 583fd2f20..92357fdce 100644 --- a/types/basetypes/string_value_test.go +++ b/types/basetypes/string_value_test.go @@ -34,7 +34,6 @@ func TestStringValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() ctx := context.Background() @@ -112,7 +111,6 @@ func TestStringValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -146,8 +144,6 @@ func TestStringValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -182,8 +178,6 @@ func TestStringValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -231,7 +225,6 @@ func TestStringValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -265,8 +258,6 @@ func TestStringValueValueString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,8 +292,6 @@ func TestStringValueValueStringPointer(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -333,8 +322,6 @@ func TestNewStringPointerValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/tuple_type_test.go b/types/basetypes/tuple_type_test.go index 65557abbd..18fa78005 100644 --- a/types/basetypes/tuple_type_test.go +++ b/types/basetypes/tuple_type_test.go @@ -98,7 +98,6 @@ func TestTupleTypeEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -151,8 +150,6 @@ func TestTupleTypeString(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -249,7 +246,6 @@ func TestTupleTypeTerraformType(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -438,7 +434,6 @@ func TestTupleTypeValueFromTerraform(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/types/basetypes/tuple_value_test.go b/types/basetypes/tuple_value_test.go index 34512babf..5736ed262 100644 --- a/types/basetypes/tuple_value_test.go +++ b/types/basetypes/tuple_value_test.go @@ -99,8 +99,6 @@ func TestNewTupleValue(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -142,8 +140,6 @@ func TestTupleValueElements(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -189,8 +185,6 @@ func TestTupleValueElementType(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -398,7 +392,6 @@ func TestTupleValueEqual(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -432,8 +425,6 @@ func TestTupleValueIsNull(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -468,8 +459,6 @@ func TestTupleValueIsUnknown(t *testing.T) { } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -556,7 +545,6 @@ func TestTupleValueString(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -635,7 +623,6 @@ func TestTupleValueType(t *testing.T) { } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() @@ -717,7 +704,6 @@ func TestTupleValueToTerraformValue(t *testing.T) { }, } for name, test := range tests { - name, test := name, test t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/website/README.md b/website/README.md index e987f6636..785ebd354 100644 --- a/website/README.md +++ b/website/README.md @@ -1,5 +1,8 @@ # Terraform Documentation +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the `hashicorp/web-unified-docs` repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + This directory contains the portions of [the Terraform website][terraform.io] that pertain to the Terraform Plugin Framework. The files in this directory are intended to be used in conjunction with diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 2297fad3c..b41e7901f 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -98,6 +98,10 @@ { "title": "Timeouts", "path": "resources/timeouts" + }, + { + "title": "Write-only Arguments", + "path": "resources/write-only-arguments" } ] }, diff --git a/website/docs/plugin/framework/acctests.mdx b/website/docs/plugin/framework/acctests.mdx index e23cd6fbb..bb825d24f 100644 --- a/website/docs/plugin/framework/acctests.mdx +++ b/website/docs/plugin/framework/acctests.mdx @@ -6,6 +6,9 @@ description: >- Terraform operations. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Acceptance tests Implement provider resource and data source acceptance tests with the [terraform-plugin-testing module](/terraform/plugin/testing). These tests are designed to execute Terraform commands against real Terraform configurations, simulating practitioner experiences with creating, refreshing, updating, and deleting infrastructure. diff --git a/website/docs/plugin/framework/data-sources/configure.mdx b/website/docs/plugin/framework/data-sources/configure.mdx index 11dda382b..c751abac7 100644 --- a/website/docs/plugin/framework/data-sources/configure.mdx +++ b/website/docs/plugin/framework/data-sources/configure.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the `hashicorp/web-unified-docs` repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Configure data sources [Data sources](/terraform/plugin/framework/data-sources) may require provider-level data or remote system clients to operate correctly. The framework supports the ability to configure this data and/or clients once within the provider, then pass that information to data sources by adding the `Configure` method. diff --git a/website/docs/plugin/framework/data-sources/index.mdx b/website/docs/plugin/framework/data-sources/index.mdx index 64dfb2ecc..28b00b2c4 100644 --- a/website/docs/plugin/framework/data-sources/index.mdx +++ b/website/docs/plugin/framework/data-sources/index.mdx @@ -5,6 +5,9 @@ description: >- framework can help you implement data sources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Data sources [Data sources](/terraform/language/data-sources) are an abstraction that allow Terraform to reference external data. Unlike [managed resources](/terraform/language/resources), Terraform does not manage the lifecycle of the resource or data. Data sources are intended to have no side-effects. diff --git a/website/docs/plugin/framework/data-sources/timeouts.mdx b/website/docs/plugin/framework/data-sources/timeouts.mdx index 7868dd402..5b4128d77 100644 --- a/website/docs/plugin/framework/data-sources/timeouts.mdx +++ b/website/docs/plugin/framework/data-sources/timeouts.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement timeouts with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Timeouts The reality of cloud infrastructure is that it typically takes time to perform operations such as booting operating systems, discovering services, and replicating state across network edges. As the provider developer you should take known delays in data source APIs into account in the `Read` function of the data source. Terraform supports configurable timeouts to assist in these situations. diff --git a/website/docs/plugin/framework/data-sources/validate-configuration.mdx b/website/docs/plugin/framework/data-sources/validate-configuration.mdx index 0a0a55dc6..df57f6d4b 100644 --- a/website/docs/plugin/framework/data-sources/validate-configuration.mdx +++ b/website/docs/plugin/framework/data-sources/validate-configuration.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Validate data source configurations [Data sources](/terraform/plugin/framework/data-sources) support validating an entire practitioner configuration in either declarative or imperative logic. Feedback, such as required syntax or acceptable combinations of values, is returned via [diagnostics](/terraform/plugin/framework/diagnostics). diff --git a/website/docs/plugin/framework/debugging.mdx b/website/docs/plugin/framework/debugging.mdx index ddae1484b..2efeec9f6 100644 --- a/website/docs/plugin/framework/debugging.mdx +++ b/website/docs/plugin/framework/debugging.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement debugger support in framework Terraform providers. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Debugging framework Providers This page contains implementation details for inspecting runtime information of a Terraform provider developed with Framework via a debugger tool by adjusting the [provider server](/terraform/plugin/framework/provider-servers) implementation. Review the top level [Debugging](/terraform/plugin/debugging) page for information pertaining to the overall Terraform provider debugging process and other inspection options, such as log-based debugging. diff --git a/website/docs/plugin/framework/deprecations.mdx b/website/docs/plugin/framework/deprecations.mdx index c86ce0ae0..c05eb08ca 100644 --- a/website/docs/plugin/framework/deprecations.mdx +++ b/website/docs/plugin/framework/deprecations.mdx @@ -5,6 +5,9 @@ description: renames in framework providers. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Deprecations, removals, and renames Terraform is trusted for managing many facets of infrastructure across many organizations. Part of that trust is due to consistent versioning guidelines and setting expectations for various levels of upgrades. Ensuring backwards compatibility for all patch and minor releases, potentially in concert with any upcoming major changes, is recommended and supported by the Terraform development framework. This allows operators to iteratively update their Terraform configurations rather than require massive refactoring. diff --git a/website/docs/plugin/framework/diagnostics.mdx b/website/docs/plugin/framework/diagnostics.mdx index 7208d4845..6a4959c6c 100644 --- a/website/docs/plugin/framework/diagnostics.mdx +++ b/website/docs/plugin/framework/diagnostics.mdx @@ -4,6 +4,9 @@ description: >- Learn how to return errors and warnings from the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Returning errors and warnings Providers use `Diagnostics` to surface errors and warnings to practitioners, diff --git a/website/docs/plugin/framework/ephemeral-resources/close.mdx b/website/docs/plugin/framework/ephemeral-resources/close.mdx index dade8f2bd..ee22fe573 100644 --- a/website/docs/plugin/framework/ephemeral-resources/close.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/close.mdx @@ -4,6 +4,9 @@ description: >- Learn how to close ephemeral resource in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Closing Ephemeral Resources Close is an optional part of the Terraform lifecycle for an ephemeral resource, which is different from the [managed resource lifecycle](https://github.com/hashicorp/terraform/blob/main/docs/resource-instance-change-lifecycle.md). During any Terraform operation (like [`terraform plan`](/terraform/cli/commands/plan) or [`terraform apply`](/terraform/cli/commands/apply)), when an ephemeral resource's data is needed, Terraform initially retrieves that data with the [`Open`](/terraform/plugin/framework/ephemeral-resources/open) lifecycle handler. Once the ephemeral resource data is no longer needed, Terraform calls the provider `CloseEphemeralResource` RPC, in which the framework calls the [`ephemeral.EphemeralResourceWithClose` interface `Close` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithClose). The request contains any `Private` data set in the latest `Open` or `Renew` call. diff --git a/website/docs/plugin/framework/ephemeral-resources/configure.mdx b/website/docs/plugin/framework/ephemeral-resources/configure.mdx index fef8e4a12..6da5694d0 100644 --- a/website/docs/plugin/framework/ephemeral-resources/configure.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/configure.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Configuring ephemeral resources [Ephemeral Resources](/terraform/plugin/framework/ephemeral-resources) may require provider-level data or remote system clients to operate correctly. The framework supports the ability to configure this data and/or clients once within the provider, then pass that information to ephemeral resources by adding the `Configure` method. diff --git a/website/docs/plugin/framework/ephemeral-resources/index.mdx b/website/docs/plugin/framework/ephemeral-resources/index.mdx index 194d65d5f..39db7f771 100644 --- a/website/docs/plugin/framework/ephemeral-resources/index.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/index.mdx @@ -6,6 +6,9 @@ description: >- to implement ephemeral resources in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Ephemeral resources diff --git a/website/docs/plugin/framework/ephemeral-resources/open.mdx b/website/docs/plugin/framework/ephemeral-resources/open.mdx index 3f7ff646a..27e2b61c8 100644 --- a/website/docs/plugin/framework/ephemeral-resources/open.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/open.mdx @@ -4,6 +4,9 @@ description: >- Learn how to open ephemeral resource in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Opening ephemeral resources Open is part of the Terraform lifecycle for an ephemeral resource, which is different from the [managed resource lifecycle](https://github.com/hashicorp/terraform/blob/main/docs/resource-instance-change-lifecycle.md). During any Terraform operation (like [`terraform plan`](/terraform/cli/commands/plan) or [`terraform apply`](/terraform/cli/commands/apply)), when an ephemeral resource's data is needed, Terraform calls the provider `OpenEphemeralResource` RPC, in which the framework calls the [`ephemeral.EphemeralResource` interface `Open` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource.Open). The request contains the configuration supplied to Terraform for the ephemeral resource. The response contains the ephemeral result data. The data is defined by the [schema](/terraform/plugin/framework/handling-data/schemas) of the ephemeral resource. diff --git a/website/docs/plugin/framework/ephemeral-resources/renew.mdx b/website/docs/plugin/framework/ephemeral-resources/renew.mdx index dadd729f7..19582ec3d 100644 --- a/website/docs/plugin/framework/ephemeral-resources/renew.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/renew.mdx @@ -4,6 +4,9 @@ description: >- Learn how to renew ephemeral resource in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Renewing ephemeral resources Renew is an optional part of the Terraform lifecycle for an ephemeral resource, which is different from the [managed resource lifecycle](https://github.com/hashicorp/terraform/blob/main/docs/resource-instance-change-lifecycle.md). During any Terraform operation (like [`terraform plan`](/terraform/cli/commands/plan) or [`terraform apply`](/terraform/cli/commands/apply)), when an ephemeral resource's data is needed, Terraform initially retrieves that data with the [`Open`](/terraform/plugin/framework/ephemeral-resources/open) lifecycle handler. During `Open`, ephemeral resources can opt to include a timestamp in the `RenewAt` response field to indicate to Terraform when a provider must renew an ephemeral resource. If an ephemeral resource's data is still in-use and the `RenewAt` timestamp has passed, Terraform calls the provider `RenewEphemeralResource` RPC, in which the framework calls the [`ephemeral.EphemeralResourceWithRenew` interface `Renew` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithRenew). The request contains any `Private` data set in the latest `Open` or `Renew` call. The response contains `Private` data and an optional `RenewAt` field for further renew executions. diff --git a/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx b/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx index 242dd4cfc..fd418a9d0 100644 --- a/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx +++ b/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Validate ephemeral resource configurations [Ephemeral resources](/terraform/plugin/framework/ephemeral-resources) support validating an entire practitioner configuration in either declarative or imperative logic. Feedback, such as required syntax or acceptable combinations of values, is returned via [diagnostics](/terraform/plugin/framework/diagnostics). diff --git a/website/docs/plugin/framework/functions/concepts.mdx b/website/docs/plugin/framework/functions/concepts.mdx index 23835d778..ab0fe40ec 100644 --- a/website/docs/plugin/framework/functions/concepts.mdx +++ b/website/docs/plugin/framework/functions/concepts.mdx @@ -5,6 +5,9 @@ description: >- functions for practitions to use in their Terraform configurations. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Provider-defined functions This page describes Terraform concepts relating to provider-defined functions within framework-based provider code. Provider-defined functions are supported in Terraform 1.8 and later. The [What is Terraform](/terraform/intro), [Terraform language](/terraform/language), and [Plugin Development](/terraform/plugin) documentation covers more general concepts behind Terraform's workflow, its configuration, and how it interacts with providers. diff --git a/website/docs/plugin/framework/functions/documentation.mdx b/website/docs/plugin/framework/functions/documentation.mdx index fd731ae0f..07825ebd4 100644 --- a/website/docs/plugin/framework/functions/documentation.mdx +++ b/website/docs/plugin/framework/functions/documentation.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Documenting functions When a function is [implemented](/terraform/plugin/framework/functions/implementation), ensure the function is discoverable by practitioners with usage information. diff --git a/website/docs/plugin/framework/functions/errors.mdx b/website/docs/plugin/framework/functions/errors.mdx index 64dc74931..0266e4ee9 100644 --- a/website/docs/plugin/framework/functions/errors.mdx +++ b/website/docs/plugin/framework/functions/errors.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Returning errors from function Providers use [`FuncError`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#FuncError) to diff --git a/website/docs/plugin/framework/functions/implementation.mdx b/website/docs/plugin/framework/functions/implementation.mdx index c811870ee..116a408af 100644 --- a/website/docs/plugin/framework/functions/implementation.mdx +++ b/website/docs/plugin/framework/functions/implementation.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Implement provider-defined functions The framework supports implementing functions based on Terraform's [concepts for provider-defined functions](/terraform/plugin/framework/functions/concepts). It is recommended to understand those concepts before implementing a function since the terminology is used throughout this page and there are details that simplify function handling as compared to other provider concepts. Provider-defined functions are supported in Terraform 1.8 and later. diff --git a/website/docs/plugin/framework/functions/index.mdx b/website/docs/plugin/framework/functions/index.mdx index 193c4ea1f..25339bea7 100644 --- a/website/docs/plugin/framework/functions/index.mdx +++ b/website/docs/plugin/framework/functions/index.mdx @@ -6,6 +6,9 @@ description: >- plugin framework can help you implement provider-defined functions. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Provider-defined functions diff --git a/website/docs/plugin/framework/functions/parameters/bool.mdx b/website/docs/plugin/framework/functions/parameters/bool.mdx index 3bf687562..af15774e0 100644 --- a/website/docs/plugin/framework/functions/parameters/bool.mdx +++ b/website/docs/plugin/framework/functions/parameters/bool.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Boolean function parameters Bool function parameters expect a boolean true or false value from a practitioner configuration. Values are accessible in function logic by the Go built-in `bool` type, Go built-in `*bool` type, or the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). diff --git a/website/docs/plugin/framework/functions/parameters/dynamic.mdx b/website/docs/plugin/framework/functions/parameters/dynamic.mdx index e088e5c55..6e66cdeba 100644 --- a/website/docs/plugin/framework/functions/parameters/dynamic.mdx +++ b/website/docs/plugin/framework/functions/parameters/dynamic.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Dynamic function parameters diff --git a/website/docs/plugin/framework/functions/parameters/float32.mdx b/website/docs/plugin/framework/functions/parameters/float32.mdx index 423df0cb2..0ffcc9bc9 100644 --- a/website/docs/plugin/framework/functions/parameters/float32.mdx +++ b/website/docs/plugin/framework/functions/parameters/float32.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float32 Function Parameter diff --git a/website/docs/plugin/framework/functions/parameters/float64.mdx b/website/docs/plugin/framework/functions/parameters/float64.mdx index 7dbe213ac..95b0c7812 100644 --- a/website/docs/plugin/framework/functions/parameters/float64.mdx +++ b/website/docs/plugin/framework/functions/parameters/float64.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float64 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/index.mdx b/website/docs/plugin/framework/functions/parameters/index.mdx index 76b4e6ed2..32a50f55f 100644 --- a/website/docs/plugin/framework/functions/parameters/index.mdx +++ b/website/docs/plugin/framework/functions/parameters/index.mdx @@ -6,6 +6,9 @@ description: >- arguments in a function definition. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Function parameters Parameters in [function definitions](/terraform/plugin/framework/functions/implementation#definition-method) describes how data values are passed to the function logic. Every parameter type has an associated [value type](/terraform/plugin/framework/handling-data/types), although this data handling is simplified for function implementations over other provider concepts, such as resource implementations. diff --git a/website/docs/plugin/framework/functions/parameters/int32.mdx b/website/docs/plugin/framework/functions/parameters/int32.mdx index de87e82b1..b08c57fc1 100644 --- a/website/docs/plugin/framework/functions/parameters/int32.mdx +++ b/website/docs/plugin/framework/functions/parameters/int32.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int32 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/int64.mdx b/website/docs/plugin/framework/functions/parameters/int64.mdx index 5e8411516..23e0e5d6a 100644 --- a/website/docs/plugin/framework/functions/parameters/int64.mdx +++ b/website/docs/plugin/framework/functions/parameters/int64.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int64 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/list.mdx b/website/docs/plugin/framework/functions/parameters/list.mdx index edbca773a..18f075209 100644 --- a/website/docs/plugin/framework/functions/parameters/list.mdx +++ b/website/docs/plugin/framework/functions/parameters/list.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List function parameters List function parameters expect an ordered collection of single element type value from a practitioner configuration. Values are accessible in function logic by a Go slice of an appropriate pointer type to match the element type `[]*T` or the [framework list type](/terraform/plugin/framework/handling-data/types/list). diff --git a/website/docs/plugin/framework/functions/parameters/map.mdx b/website/docs/plugin/framework/functions/parameters/map.mdx index 49d35d893..38430df0b 100644 --- a/website/docs/plugin/framework/functions/parameters/map.mdx +++ b/website/docs/plugin/framework/functions/parameters/map.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List function parameters Map function parameters expect a mapping of arbitrary string keys to values of single element type from a practitioner configuration. Values are accessible in function logic by a Go map of string keys to values of an appropriate pointer type to match the element type `map[string]*T` or the [framework map type](/terraform/plugin/framework/handling-data/types/map). diff --git a/website/docs/plugin/framework/functions/parameters/number.mdx b/website/docs/plugin/framework/functions/parameters/number.mdx index 256d521ef..0e385a564 100644 --- a/website/docs/plugin/framework/functions/parameters/number.mdx +++ b/website/docs/plugin/framework/functions/parameters/number.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Number function parameters diff --git a/website/docs/plugin/framework/functions/parameters/object.mdx b/website/docs/plugin/framework/functions/parameters/object.mdx index 41bdcbae4..2d687b6a6 100644 --- a/website/docs/plugin/framework/functions/parameters/object.mdx +++ b/website/docs/plugin/framework/functions/parameters/object.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Object function parameters Object function parameters expect a single structure mapping explicit attribute names to type definitions from a practitioner configuration. Values are accessible in function logic by a Go structure type annotated with `tfsdk` field tags or the [framework object type](/terraform/plugin/framework/handling-data/types/object). diff --git a/website/docs/plugin/framework/functions/parameters/set.mdx b/website/docs/plugin/framework/functions/parameters/set.mdx index dc53f9952..33f98133d 100644 --- a/website/docs/plugin/framework/functions/parameters/set.mdx +++ b/website/docs/plugin/framework/functions/parameters/set.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set function parameters Set function parameters expect an unordered, unique collection of single element type value from a practitioner configuration. Values are accessible in function logic by a Go slice of an appropriate pointer type to match the element type `[]*T` or the [framework set type](/terraform/plugin/framework/handling-data/types/set). diff --git a/website/docs/plugin/framework/functions/parameters/string.mdx b/website/docs/plugin/framework/functions/parameters/string.mdx index bb449009c..43cc7f9d0 100644 --- a/website/docs/plugin/framework/functions/parameters/string.mdx +++ b/website/docs/plugin/framework/functions/parameters/string.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # String function parameters String function parameters expect a collection of UTF-8 encoded bytes from a practitioner configuration. Values are accessible in function logic by the Go built-in `string` type, Go built-in `*string` type, or the [framework string type](/terraform/plugin/framework/handling-data/types/string). diff --git a/website/docs/plugin/framework/functions/returns/bool.mdx b/website/docs/plugin/framework/functions/returns/bool.mdx index 0da2af0d0..e3514a164 100644 --- a/website/docs/plugin/framework/functions/returns/bool.mdx +++ b/website/docs/plugin/framework/functions/returns/bool.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Boolean return values Bool function return values expect a boolean true or false value from function logic. Set values in function logic with the Go built-in `bool` type, Go built-in `*bool` type, or the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). diff --git a/website/docs/plugin/framework/functions/returns/dynamic.mdx b/website/docs/plugin/framework/functions/returns/dynamic.mdx index 844d5926a..56ebd61cf 100644 --- a/website/docs/plugin/framework/functions/returns/dynamic.mdx +++ b/website/docs/plugin/framework/functions/returns/dynamic.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Dynamic function return values diff --git a/website/docs/plugin/framework/functions/returns/float32.mdx b/website/docs/plugin/framework/functions/returns/float32.mdx index fbd0f48e5..7cbf562e3 100644 --- a/website/docs/plugin/framework/functions/returns/float32.mdx +++ b/website/docs/plugin/framework/functions/returns/float32.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float32 return values diff --git a/website/docs/plugin/framework/functions/returns/float64.mdx b/website/docs/plugin/framework/functions/returns/float64.mdx index 2769c1d98..2dc241b3e 100644 --- a/website/docs/plugin/framework/functions/returns/float64.mdx +++ b/website/docs/plugin/framework/functions/returns/float64.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float64 return values diff --git a/website/docs/plugin/framework/functions/returns/index.mdx b/website/docs/plugin/framework/functions/returns/index.mdx index a81c5bc83..46c6f1b12 100644 --- a/website/docs/plugin/framework/functions/returns/index.mdx +++ b/website/docs/plugin/framework/functions/returns/index.mdx @@ -6,6 +6,9 @@ description: >- data in a function definition. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Return values A return in a [function definition](/terraform/plugin/framework/functions/implementation#definition-method) describes the result data value from function logic. Every return type has an associated [value type](/terraform/plugin/framework/handling-data/types), although this data handling is simplified for function implementations over other provider concepts, such as resource implementations. diff --git a/website/docs/plugin/framework/functions/returns/int32.mdx b/website/docs/plugin/framework/functions/returns/int32.mdx index b0c4b0c54..4c016a18d 100644 --- a/website/docs/plugin/framework/functions/returns/int32.mdx +++ b/website/docs/plugin/framework/functions/returns/int32.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int32 return values diff --git a/website/docs/plugin/framework/functions/returns/int64.mdx b/website/docs/plugin/framework/functions/returns/int64.mdx index a134eda66..b9dfa0a15 100644 --- a/website/docs/plugin/framework/functions/returns/int64.mdx +++ b/website/docs/plugin/framework/functions/returns/int64.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int64 return values diff --git a/website/docs/plugin/framework/functions/returns/list.mdx b/website/docs/plugin/framework/functions/returns/list.mdx index 39cc953c7..f2f5a3a6e 100644 --- a/website/docs/plugin/framework/functions/returns/list.mdx +++ b/website/docs/plugin/framework/functions/returns/list.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List return values List function return expects an ordered collection of single element type value from function logic. Set values in function logic with a Go slice of an appropriate type to match the element type `[]T` or the [framework list type](/terraform/plugin/framework/handling-data/types/list). diff --git a/website/docs/plugin/framework/functions/returns/map.mdx b/website/docs/plugin/framework/functions/returns/map.mdx index 3ae20085a..ce9d27208 100644 --- a/website/docs/plugin/framework/functions/returns/map.mdx +++ b/website/docs/plugin/framework/functions/returns/map.mdx @@ -5,6 +5,9 @@ description: >- Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Map return values Map function return expects a mapping of arbitrary string keys to values of single element type from function logic. Set values in function logic with a Go map of string keys to values of an appropriate type to match the element type `map[string]T` or the [framework map type](/terraform/plugin/framework/handling-data/types/map). diff --git a/website/docs/plugin/framework/functions/returns/number.mdx b/website/docs/plugin/framework/functions/returns/number.mdx index 74e6a2c01..21fb1cb60 100644 --- a/website/docs/plugin/framework/functions/returns/number.mdx +++ b/website/docs/plugin/framework/functions/returns/number.mdx @@ -5,6 +5,9 @@ description: >- with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Number return values diff --git a/website/docs/plugin/framework/functions/returns/object.mdx b/website/docs/plugin/framework/functions/returns/object.mdx index 7f4a354a3..383ae33bc 100644 --- a/website/docs/plugin/framework/functions/returns/object.mdx +++ b/website/docs/plugin/framework/functions/returns/object.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Object return values Object function return expects a single structure mapping explicit attribute names to type definitions from function logic. Set values in function logic with a Go structure type annotated with `tfsdk` field tags or the [framework map type](/terraform/plugin/framework/handling-data/types/map). diff --git a/website/docs/plugin/framework/functions/returns/set.mdx b/website/docs/plugin/framework/functions/returns/set.mdx index 1c32dc777..cda4deb41 100644 --- a/website/docs/plugin/framework/functions/returns/set.mdx +++ b/website/docs/plugin/framework/functions/returns/set.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set return values Set function return expects an unordered, unique collection of single element type value from function logic. Set values in function logic with a Go slice of an appropriate type to match the element type `[]T` or the [framework set type](/terraform/plugin/framework/handling-data/types/set). diff --git a/website/docs/plugin/framework/functions/returns/string.mdx b/website/docs/plugin/framework/functions/returns/string.mdx index d80f9fa5b..7d4cd6e61 100644 --- a/website/docs/plugin/framework/functions/returns/string.mdx +++ b/website/docs/plugin/framework/functions/returns/string.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # String return values String function return expects a collection of UTF-8 encoded bytes from function logic. Set values in function logic with the Go built-in `string` type, Go built-in `*string` type, or the [framework string type](/terraform/plugin/framework/handling-data/types/string). diff --git a/website/docs/plugin/framework/functions/testing.mdx b/website/docs/plugin/framework/functions/testing.mdx index dad993547..286131014 100644 --- a/website/docs/plugin/framework/functions/testing.mdx +++ b/website/docs/plugin/framework/functions/testing.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Testing functions When a function is [implemented](/terraform/plugin/framework/functions/implementation), ensure the function behaves as expected. Follow [recommendations](#recommendations) to cover how practitioner configurations may call the function. diff --git a/website/docs/plugin/framework/getting-started/code-walkthrough.mdx b/website/docs/plugin/framework/getting-started/code-walkthrough.mdx index 8aecf5506..9dceee50f 100644 --- a/website/docs/plugin/framework/getting-started/code-walkthrough.mdx +++ b/website/docs/plugin/framework/getting-started/code-walkthrough.mdx @@ -6,6 +6,9 @@ description: >- by exploring its main components and libraries. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Provider code walkthrough [Terraform providers](/terraform/language/providers) let Terraform communicate with third parties, such as cloud providers, SaaS providers, and other APIs. Terraform and Terraform providers use gRPC to communicate. Terraform operates as a gRPC client and providers operate as gRPC servers. diff --git a/website/docs/plugin/framework/handling-data/accessing-values.mdx b/website/docs/plugin/framework/handling-data/accessing-values.mdx index a3d1777f4..728d68457 100644 --- a/website/docs/plugin/framework/handling-data/accessing-values.mdx +++ b/website/docs/plugin/framework/handling-data/accessing-values.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Access state, configuration, and plan data There are various points at which the provider needs access to the data from diff --git a/website/docs/plugin/framework/handling-data/attributes/bool.mdx b/website/docs/plugin/framework/handling-data/attributes/bool.mdx index 576baff50..ee8f93c39 100644 --- a/website/docs/plugin/framework/handling-data/attributes/bool.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/bool.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use boolean attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Boolean attributes @@ -105,6 +108,18 @@ The [`boolplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx index 44701cc24..0c13c3eb2 100644 --- a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use dynamic attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Dynamic attribute @@ -134,6 +137,18 @@ The [`dynamicplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float32.mdx b/website/docs/plugin/framework/handling-data/attributes/float32.mdx index 5f710dafe..2dd15ddc4 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float32 attributes @@ -111,6 +114,18 @@ The [`float32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float64.mdx b/website/docs/plugin/framework/handling-data/attributes/float64.mdx index da31351b6..d3838a33c 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float64 attributes @@ -111,6 +114,18 @@ The [`float64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/index.mdx b/website/docs/plugin/framework/handling-data/attributes/index.mdx index 58397eacb..16fad163a 100644 --- a/website/docs/plugin/framework/handling-data/attributes/index.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/index.mdx @@ -7,6 +7,9 @@ description: >- custom type. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Attributes Attributes are value storing fields in resource, data source, or provider [schemas](/terraform/plugin/framework/handling-data/schemas). Every attribute has an associated [value type](/terraform/plugin/framework/handling-data/types), which describes the kind of data the attribute can hold. Attributes also can describe value plan modifiers (resources only) and value validators in addition to those defined by the value type. diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx index f0c4ad56c..7a2c9268d 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int32 attributes @@ -111,6 +114,18 @@ The [`int32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/int64.mdx b/website/docs/plugin/framework/handling-data/attributes/int64.mdx index 416976e45..5eb9970a0 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int64 attributes @@ -111,6 +114,18 @@ The [`int64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx index ea0272133..ed328e6fc 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use list nested attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List nested attributes List nested attributes store an ordered collection of nested objects. Values are represented by a [list type](/terraform/plugin/framework/handling-data/types/list) in the framework, containing elements of [object type](/terraform/plugin/framework/handling-data/types/object). @@ -159,6 +162,20 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list.mdx b/website/docs/plugin/framework/handling-data/attributes/list.mdx index 2104e2475..d05788d9a 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use list attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List attributes List attributes store an ordered collection of single element type. Values are represented by a [list type](/terraform/plugin/framework/handling-data/types/list) in the framework, containing elements of the element type. @@ -128,6 +131,18 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx index 210f25a9d..3a20d2b54 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use map nested attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Map Nested Attribute Map nested attributes store mapping of arbitrary string keys to nested objects. Values are represented by a [map type](/terraform/plugin/framework/handling-data/types/map) in the framework, containing elements of [object type](/terraform/plugin/framework/handling-data/types/object). @@ -159,6 +162,20 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map.mdx b/website/docs/plugin/framework/handling-data/attributes/map.mdx index 7eb65aa89..50c1bf468 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use map attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Map attributes Map attributes store a mapping of arbitrary string keys to values of single element type. Values are represented by a [map type](/terraform/plugin/framework/handling-data/types/map) in the framework, containing elements of the element type. @@ -131,6 +134,18 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/number.mdx b/website/docs/plugin/framework/handling-data/attributes/number.mdx index 4fb48a1ba..a5c81e44c 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Number attributes @@ -111,6 +114,18 @@ The [`numberplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/object.mdx b/website/docs/plugin/framework/handling-data/attributes/object.mdx index d7f2c837d..2e1b5e5a5 100644 --- a/website/docs/plugin/framework/handling-data/attributes/object.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/object.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use object attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Object attributes @@ -165,6 +168,18 @@ Only the object attribute itself, not individual sub-attributes, can define its Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx index 01ce7e51f..ef5a577ed 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use set nested attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set nested attributes Set nested attributes store a unique, unordered collection of nested objects. Values are represented by a [set type](/terraform/plugin/framework/handling-data/types/set) in the framework, containing elements of [object type](/terraform/plugin/framework/handling-data/types/object). diff --git a/website/docs/plugin/framework/handling-data/attributes/set.mdx b/website/docs/plugin/framework/handling-data/attributes/set.mdx index 512489aad..17caaf849 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use set attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set attributes Set attributes store an unique, unordered collection of single element type. Values are represented by a [set type](/terraform/plugin/framework/handling-data/types/set) in the framework, containing elements of the element type. diff --git a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx index 56675912f..a628ea4b6 100644 --- a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use single nested attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Single nested attributes Single nested attributes are a single structure mapping explicit attribute names to nested attribute definitions. Values are represented by a [object type](/terraform/plugin/framework/handling-data/types/object) in the framework, containing nested attribute values of the mapped attributes. @@ -155,6 +158,20 @@ The [`objectplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/string.mdx b/website/docs/plugin/framework/handling-data/attributes/string.mdx index d423109dd..fec7a654b 100644 --- a/website/docs/plugin/framework/handling-data/attributes/string.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/string.mdx @@ -4,6 +4,9 @@ description: >- Learn how to use string attributes with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # String attributes String attributes store a collection of UTF-8 encoded bytes. Values are represented by a [string type](/terraform/plugin/framework/handling-data/types/string) in the framework. @@ -112,6 +115,18 @@ The [`stringplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/blocks/index.mdx b/website/docs/plugin/framework/handling-data/blocks/index.mdx index af96c5021..20b9a129c 100644 --- a/website/docs/plugin/framework/handling-data/blocks/index.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/index.mdx @@ -6,6 +6,9 @@ description: >- provider schema. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Blocks diff --git a/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx b/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx index e17c362f4..04b9d7aff 100644 --- a/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List nested blocks diff --git a/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx b/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx index e780d5b5c..bea59187c 100644 --- a/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx @@ -4,6 +4,9 @@ description: >- Learn to implement the set nested block type with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set nested blocks diff --git a/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx b/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx index e078b1103..3911a0f7d 100644 --- a/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Single nested blocks diff --git a/website/docs/plugin/framework/handling-data/dynamic-data.mdx b/website/docs/plugin/framework/handling-data/dynamic-data.mdx index 07f845f41..921a6f6da 100644 --- a/website/docs/plugin/framework/handling-data/dynamic-data.mdx +++ b/website/docs/plugin/framework/handling-data/dynamic-data.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Handling dynamic data diff --git a/website/docs/plugin/framework/handling-data/path-expressions.mdx b/website/docs/plugin/framework/handling-data/path-expressions.mdx index 80cca2a1d..dc34c28a9 100644 --- a/website/docs/plugin/framework/handling-data/path-expressions.mdx +++ b/website/docs/plugin/framework/handling-data/path-expressions.mdx @@ -6,6 +6,9 @@ description: >- more actual paths within schema data. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Path expressions diff --git a/website/docs/plugin/framework/handling-data/paths.mdx b/website/docs/plugin/framework/handling-data/paths.mdx index 1df50cf70..b887da83a 100644 --- a/website/docs/plugin/framework/handling-data/paths.mdx +++ b/website/docs/plugin/framework/handling-data/paths.mdx @@ -5,6 +5,9 @@ description: >- represent a location within a schema or schema-based data. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Paths An exact location within a [schema](/terraform/plugin/framework/handling-data/schemas) or schema-based data such as [`tfsdk.Config`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#Config), [`tfsdk.Plan`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#Plan), or [`tfsdk.State`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/tfsdk#State), is referred to as a path. diff --git a/website/docs/plugin/framework/handling-data/schemas.mdx b/website/docs/plugin/framework/handling-data/schemas.mdx index e406565e5..d69c26c50 100644 --- a/website/docs/plugin/framework/handling-data/schemas.mdx +++ b/website/docs/plugin/framework/handling-data/schemas.mdx @@ -5,6 +5,9 @@ description: >- specify the constraints of Terraform configuration blocks. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Schemas Schemas specify the constraints of Terraform configuration blocks. They define what fields a provider, diff --git a/website/docs/plugin/framework/handling-data/terraform-concepts.mdx b/website/docs/plugin/framework/handling-data/terraform-concepts.mdx index d8602e8a3..2d3188289 100644 --- a/website/docs/plugin/framework/handling-data/terraform-concepts.mdx +++ b/website/docs/plugin/framework/handling-data/terraform-concepts.mdx @@ -5,6 +5,9 @@ description: >- configuration to schemas, attributes, and blocks. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform data concepts This page describes Terraform concepts as they relate to handling data within framework-based provider code. The [What is Terraform](/terraform/intro), [Terraform language](/terraform/language), and [Plugin Development](/terraform/plugin) documentation covers more general concepts behind Terraform's workflow, its configuration, and how it interacts with providers. diff --git a/website/docs/plugin/framework/handling-data/types/bool.mdx b/website/docs/plugin/framework/handling-data/types/bool.mdx index d771afd10..981ebe449 100644 --- a/website/docs/plugin/framework/handling-data/types/bool.mdx +++ b/website/docs/plugin/framework/handling-data/types/bool.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement boolean value types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Bool types Bool types store a boolean true or false value. diff --git a/website/docs/plugin/framework/handling-data/types/custom.mdx b/website/docs/plugin/framework/handling-data/types/custom.mdx index ad47f3372..188bab2f5 100644 --- a/website/docs/plugin/framework/handling-data/types/custom.mdx +++ b/website/docs/plugin/framework/handling-data/types/custom.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement custom types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Custom types Use existing custom types or develop custom types to consistently define behaviors for a kind of value across schemas. Custom types are supported on top of any framework-defined type. diff --git a/website/docs/plugin/framework/handling-data/types/dynamic.mdx b/website/docs/plugin/framework/handling-data/types/dynamic.mdx index cd0194eef..4cb5c1315 100644 --- a/website/docs/plugin/framework/handling-data/types/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/types/dynamic.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement dynamic types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Dynamic types diff --git a/website/docs/plugin/framework/handling-data/types/float32.mdx b/website/docs/plugin/framework/handling-data/types/float32.mdx index bc3bdc8e2..8f9ad0d6f 100644 --- a/website/docs/plugin/framework/handling-data/types/float32.mdx +++ b/website/docs/plugin/framework/handling-data/types/float32.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float32 types diff --git a/website/docs/plugin/framework/handling-data/types/float64.mdx b/website/docs/plugin/framework/handling-data/types/float64.mdx index 6fc389679..08289c73d 100644 --- a/website/docs/plugin/framework/handling-data/types/float64.mdx +++ b/website/docs/plugin/framework/handling-data/types/float64.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Float64 types diff --git a/website/docs/plugin/framework/handling-data/types/index.mdx b/website/docs/plugin/framework/handling-data/types/index.mdx index 4e5f9103a..40ad90abc 100644 --- a/website/docs/plugin/framework/handling-data/types/index.mdx +++ b/website/docs/plugin/framework/handling-data/types/index.mdx @@ -6,6 +6,9 @@ description: >- types based off of the built-in attribute types. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Data types Types are value storage and access mechanism for resource, data source, or provider [schema](/terraform/plugin/framework/handling-data/schemas) data. Every attribute and block has an associated type, which describes the kind of data. These types fully support Terraform's [type system concepts](/terraform/plugin/framework/handling-data/terraform-concepts) that cannot be represented in Go built-in types, such as `*string`. Framework types can be extended by implementing [custom types](/terraform/plugin/framework/handling-data/types/custom) in provider code or shared libraries to provide specific use case functionality. diff --git a/website/docs/plugin/framework/handling-data/types/int32.mdx b/website/docs/plugin/framework/handling-data/types/int32.mdx index 4cc69ca3f..3bed7f6d5 100644 --- a/website/docs/plugin/framework/handling-data/types/int32.mdx +++ b/website/docs/plugin/framework/handling-data/types/int32.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int32 types diff --git a/website/docs/plugin/framework/handling-data/types/int64.mdx b/website/docs/plugin/framework/handling-data/types/int64.mdx index 5d4f99da8..f9c35d2c4 100644 --- a/website/docs/plugin/framework/handling-data/types/int64.mdx +++ b/website/docs/plugin/framework/handling-data/types/int64.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Int64 types diff --git a/website/docs/plugin/framework/handling-data/types/list.mdx b/website/docs/plugin/framework/handling-data/types/list.mdx index 252f1df5e..ff9d2f402 100644 --- a/website/docs/plugin/framework/handling-data/types/list.mdx +++ b/website/docs/plugin/framework/handling-data/types/list.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # List types List types store an ordered collection of single element type. diff --git a/website/docs/plugin/framework/handling-data/types/map.mdx b/website/docs/plugin/framework/handling-data/types/map.mdx index b6be68b9d..32d7edef2 100644 --- a/website/docs/plugin/framework/handling-data/types/map.mdx +++ b/website/docs/plugin/framework/handling-data/types/map.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Map type Map types store an ordered collection of single element type. diff --git a/website/docs/plugin/framework/handling-data/types/number.mdx b/website/docs/plugin/framework/handling-data/types/number.mdx index 99c7315db..f8e3a1ff5 100644 --- a/website/docs/plugin/framework/handling-data/types/number.mdx +++ b/website/docs/plugin/framework/handling-data/types/number.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Number types diff --git a/website/docs/plugin/framework/handling-data/types/object.mdx b/website/docs/plugin/framework/handling-data/types/object.mdx index b6ae89545..7e0bc9037 100644 --- a/website/docs/plugin/framework/handling-data/types/object.mdx +++ b/website/docs/plugin/framework/handling-data/types/object.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement object value types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Object types Object types store a mapping of explicit attribute names to value types. Objects must declare all attribute values, even when null or unknown, unless the entire object is null or unknown. diff --git a/website/docs/plugin/framework/handling-data/types/set.mdx b/website/docs/plugin/framework/handling-data/types/set.mdx index 73e0576cb..fac4f4826 100644 --- a/website/docs/plugin/framework/handling-data/types/set.mdx +++ b/website/docs/plugin/framework/handling-data/types/set.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement set value types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Set types Set types store an ordered collection of single element type. diff --git a/website/docs/plugin/framework/handling-data/types/string.mdx b/website/docs/plugin/framework/handling-data/types/string.mdx index c4644153d..6a3ae5333 100644 --- a/website/docs/plugin/framework/handling-data/types/string.mdx +++ b/website/docs/plugin/framework/handling-data/types/string.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement string value types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # String types String types store a collection of UTF-8 encoded bytes. diff --git a/website/docs/plugin/framework/handling-data/types/tuple.mdx b/website/docs/plugin/framework/handling-data/types/tuple.mdx index 1276559a9..feb696693 100644 --- a/website/docs/plugin/framework/handling-data/types/tuple.mdx +++ b/website/docs/plugin/framework/handling-data/types/tuple.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement tuple value types with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Tuple types diff --git a/website/docs/plugin/framework/handling-data/writing-state.mdx b/website/docs/plugin/framework/handling-data/writing-state.mdx index 200ef6501..31921c515 100644 --- a/website/docs/plugin/framework/handling-data/writing-state.mdx +++ b/website/docs/plugin/framework/handling-data/writing-state.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Writing state One of the primary jobs of a Terraform provider is to manage the provider's diff --git a/website/docs/plugin/framework/index.mdx b/website/docs/plugin/framework/index.mdx index 59379ac0c..fa481feae 100644 --- a/website/docs/plugin/framework/index.mdx +++ b/website/docs/plugin/framework/index.mdx @@ -5,6 +5,9 @@ description: >- providers. Learn how the plugin framework works with Terraform core. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Terraform plugin framework The plugin framework is HashiCorp’s recommended way develop Terraform Plugins on [protocol version 6](/terraform/plugin/terraform-plugin-protocol#protocol-version-6) or [protocol version 5](/terraform/plugin/terraform-plugin-protocol#protocol-version-5). diff --git a/website/docs/plugin/framework/internals/index.mdx b/website/docs/plugin/framework/internals/index.mdx index 3212e2b71..a6cbcc3f7 100644 --- a/website/docs/plugin/framework/internals/index.mdx +++ b/website/docs/plugin/framework/internals/index.mdx @@ -5,6 +5,9 @@ description: >- Learn about the internal implementation details of the framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Framework internals The following information describes some internals of the Terraform Plugin Framework in order to provide a more in-depth view of specific aspects of Framework behaviour. diff --git a/website/docs/plugin/framework/internals/rpcs.mdx b/website/docs/plugin/framework/internals/rpcs.mdx index 4e12a18fe..a041489ce 100644 --- a/website/docs/plugin/framework/internals/rpcs.mdx +++ b/website/docs/plugin/framework/internals/rpcs.mdx @@ -4,6 +4,9 @@ description: >- Learn how Terraform uses RPCs to support provider functionality. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # RPCs and framework functionality The correlation between the Terraform command, the RPCs that are issued and the Terraform plugin framework methods that are called is as follows: diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx index 4042f31d2..7fdae9dd4 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx @@ -5,6 +5,9 @@ description: >- the terraform-plugin-mux Go library. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute schema Attributes define how users can configure values for your Terraform provider, resources, and data sources. Refer to diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx index 1f4ee4bcc..1da120a15 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx @@ -5,6 +5,9 @@ description: >- validators in the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating blocks with computed fields Some providers, resources, and data sources include repeatable nested blocks in their attributes. Some blocks contain diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx index 353029dee..0b3a281de 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating blocks Some providers, resources, and data sources include repeatable nested blocks in their attributes. These nested blocks diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx index f358d8d62..adf349c28 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx @@ -5,6 +5,9 @@ description: >- attribute plan modifier in the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute default values Default values support is only available in the Framework for resources. Handle default values for data source attributes within the [data source `Read` method](/terraform/plugin/framework/data-sources#read-method) and default values for provider attributes within the [provider `Configure` method](/terraform/plugin/framework/providers#configure-method). diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx index 6efe29b7a..33459851b 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx @@ -5,6 +5,9 @@ description: >- fields from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute fields A subset of attribute fields, such as required, optional, computed, or sensitive, define attribute behavior as boolean flags. Refer to diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx index 1a119eff6..4a4976be6 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx @@ -5,6 +5,9 @@ description: >- modifiers in the framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute ForceNew triggers In Terraform, sometimes a resource must be replaced when the value of an attribute changes. In SDKv2, this is diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx index 86d633fa7..a29bcb47c 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate attribute type from SDKv2 to the plugin Framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute types An attribute either contains a primitive type, such as an integer or a string, or contains other attributes. Attributes diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx index 8c33f4c01..168607566 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx @@ -6,6 +6,9 @@ description: >- check attribute values for required syntax, types, and acceptable values. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating attribute custom validators You can write custom validations that give users feedback about required syntax, types, and acceptable values in your diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx index 33c32dfd4..cb015260f 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx @@ -7,6 +7,9 @@ description: >- and acceptable values. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating predefined attribute validators Attribute validators ensure that attributes do or do not contain specific values. You can use predefined validators for diff --git a/website/docs/plugin/framework/migrating/benefits.mdx b/website/docs/plugin/framework/migrating/benefits.mdx index 847b2c73f..362c271ad 100644 --- a/website/docs/plugin/framework/migrating/benefits.mdx +++ b/website/docs/plugin/framework/migrating/benefits.mdx @@ -6,6 +6,9 @@ description: >- over the previous SDKv2. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Benefits of migrating to the plugin framework We recommend using the plugin framework to develop your provider because it offers significant benefits in comparison to SDKv2. We designed the framework with feedback from thousands of existing providers, so the framework significantly improves upon the functionality available in SDKv2. diff --git a/website/docs/plugin/framework/migrating/data-sources/index.mdx b/website/docs/plugin/framework/migrating/data-sources/index.mdx index eaa78e446..747549c9a 100644 --- a/website/docs/plugin/framework/migrating/data-sources/index.mdx +++ b/website/docs/plugin/framework/migrating/data-sources/index.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate a data source from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating data sources Data sources let Terraform reference external data. Unlike resources, Terraform does not create, update, or delete diff --git a/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx b/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx index 7b0105316..8cd1d2d55 100644 --- a/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx +++ b/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate timeouts from SDKv2 to the framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating timeouts The Framework can be used in conjunction with the [terraform-plugin-framework-timeouts](https://github.com/hashicorp/terraform-plugin-framework-timeouts) module in order to allow defining timeouts in configuration and have them be available in `Read` functions. diff --git a/website/docs/plugin/framework/migrating/index.mdx b/website/docs/plugin/framework/migrating/index.mdx index f13455039..d440a5931 100644 --- a/website/docs/plugin/framework/migrating/index.mdx +++ b/website/docs/plugin/framework/migrating/index.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate your provider from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Overview This guide helps you migrate a Terraform provider from SDKv2 to the plugin Framework. We recommend migrating because the Framework has abstractions that make it easier to use, and it represents the future of Terraform plugin development. Refer to [Plugin Framework Benefits](/terraform/plugin/framework-benefits) for higher level details about how the framework makes provider development easier and [Feature Comparison](/terraform/plugin/framework/migrating/benefits) for a detailed functionality comparison between the SDKv2 and the framework. diff --git a/website/docs/plugin/framework/migrating/mux.mdx b/website/docs/plugin/framework/migrating/mux.mdx index e0e57c4b7..6bac42693 100644 --- a/website/docs/plugin/framework/migrating/mux.mdx +++ b/website/docs/plugin/framework/migrating/mux.mdx @@ -5,6 +5,9 @@ description: >- the terraform-plugin-mux Go library. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Muxing Muxing enables multiple underlying provider implementations to exist within the same logical provider server via the [terraform-plugin-mux Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-mux). Each underlying provider implementation serves different managed resources and data sources. Refer to the [Combining and Translating documentation](/terraform/plugin/mux) for full details about muxing configuration. diff --git a/website/docs/plugin/framework/migrating/providers/index.mdx b/website/docs/plugin/framework/migrating/providers/index.mdx index 6bc14ba9a..70d023623 100644 --- a/website/docs/plugin/framework/migrating/providers/index.mdx +++ b/website/docs/plugin/framework/migrating/providers/index.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating providers Providers are Terraform plugins that define resources and data sources for practitioners to use. You serve your diff --git a/website/docs/plugin/framework/migrating/resources/crud.mdx b/website/docs/plugin/framework/migrating/resources/crud.mdx index 645c0aae3..0b685b982 100644 --- a/website/docs/plugin/framework/migrating/resources/crud.mdx +++ b/website/docs/plugin/framework/migrating/resources/crud.mdx @@ -5,6 +5,9 @@ description: >- functions from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # CRUD functions In Terraform, a resource represents a single instance of a given resource type. They modify a specific resource in the diff --git a/website/docs/plugin/framework/migrating/resources/import.mdx b/website/docs/plugin/framework/migrating/resources/import.mdx index c7ebb97aa..cfae5ea13 100644 --- a/website/docs/plugin/framework/migrating/resources/import.mdx +++ b/website/docs/plugin/framework/migrating/resources/import.mdx @@ -6,6 +6,9 @@ description: >- their Terraform projects. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resource import Practitioners can use the [`terraform import` command](/terraform/cli/commands/import) to let Terraform diff --git a/website/docs/plugin/framework/migrating/resources/index.mdx b/website/docs/plugin/framework/migrating/resources/index.mdx index 73d383a3a..337b63598 100644 --- a/website/docs/plugin/framework/migrating/resources/index.mdx +++ b/website/docs/plugin/framework/migrating/resources/index.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate resources from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating resources Resources are an abstraction that allow Terraform to manage infrastructure objects by defining create, read, update, diff --git a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx index af8241279..5822be101 100644 --- a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx @@ -5,6 +5,9 @@ description: >- plan modifiers in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Plan modification Your provider can modify the Terraform plan to match the expected end state. This can include replacing unknown values diff --git a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx index 7b5c05b33..fef82a9d0 100644 --- a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx +++ b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx @@ -6,6 +6,9 @@ description: >- old schema configurations. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # State upgraders When you update a resource's implementation in your provider, some changes may not be compatible with old versions. You diff --git a/website/docs/plugin/framework/migrating/resources/timeouts.mdx b/website/docs/plugin/framework/migrating/resources/timeouts.mdx index a7a8d76c2..6b4ee3604 100644 --- a/website/docs/plugin/framework/migrating/resources/timeouts.mdx +++ b/website/docs/plugin/framework/migrating/resources/timeouts.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate timeouts from SDKv2 to the framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Timeouts The Framework can be used in conjunction with the [terraform-plugin-framework-timeouts](https://github.com/hashicorp/terraform-plugin-framework-timeouts) module in order to allow defining timeouts in configuration and have them be available in CRUD functions. diff --git a/website/docs/plugin/framework/migrating/schema/index.mdx b/website/docs/plugin/framework/migrating/schema/index.mdx index 5afd215f2..f9ffdb47f 100644 --- a/website/docs/plugin/framework/migrating/schema/index.mdx +++ b/website/docs/plugin/framework/migrating/schema/index.mdx @@ -4,6 +4,9 @@ description: >- Learn how to migrate schema from SDKv2 to the plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Migrating schema Providers, resources, and data sources all use schema to define their attributes and behavior. Schemas specify the diff --git a/website/docs/plugin/framework/migrating/testing.mdx b/website/docs/plugin/framework/migrating/testing.mdx index 79eb6ea74..3a7ae48ef 100644 --- a/website/docs/plugin/framework/migrating/testing.mdx +++ b/website/docs/plugin/framework/migrating/testing.mdx @@ -5,6 +5,9 @@ description: >- Framework does not affect provider behavior. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Testing During migration, you should [write tests](#testing-migration) to verify that the behaviour of your provider has not diff --git a/website/docs/plugin/framework/provider-servers.mdx b/website/docs/plugin/framework/provider-servers.mdx index a8d07bd5e..93c34eaac 100644 --- a/website/docs/plugin/framework/provider-servers.mdx +++ b/website/docs/plugin/framework/provider-servers.mdx @@ -6,6 +6,9 @@ description: >- APIs. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Provider servers Before a [provider](/terraform/plugin/framework/providers) can be used with Terraform, it must implement a [gRPC server](https://grpc.io) that supports Terraform-specific connection and handshake handling on startup. The server must then implement the [Terraform Plugin Protocol](/terraform/plugin/how-terraform-works#terraform-plugin-protocol). diff --git a/website/docs/plugin/framework/providers/index.mdx b/website/docs/plugin/framework/providers/index.mdx index 131047f55..b65e257b2 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -6,6 +6,9 @@ description: >- interact with APIs. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Providers Providers are Terraform plugins that define [resources](/terraform/plugin/framework/resources) and [data sources](/terraform/plugin/framework/data-sources) for practitioners to use. Providers are wrapped by a [provider server](/terraform/plugin/framework/provider-servers) for interacting with Terraform. diff --git a/website/docs/plugin/framework/providers/validate-configuration.mdx b/website/docs/plugin/framework/providers/validate-configuration.mdx index f59330f1c..7d5a49fe3 100644 --- a/website/docs/plugin/framework/providers/validate-configuration.mdx +++ b/website/docs/plugin/framework/providers/validate-configuration.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Validate provider configuration [Providers](/terraform/plugin/framework/providers) support validating an entire practitioner configuration in either declarative or imperative logic. Feedback, such as required syntax or acceptable combinations of values, is returned via [diagnostics](/terraform/plugin/framework/diagnostics). diff --git a/website/docs/plugin/framework/resources/configure.mdx b/website/docs/plugin/framework/resources/configure.mdx index 93dced950..412fd05b7 100644 --- a/website/docs/plugin/framework/resources/configure.mdx +++ b/website/docs/plugin/framework/resources/configure.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Configure resources [Resources](/terraform/plugin/framework/resources) may require provider-level data or remote system clients to operate correctly. The framework supports the ability to configure this data and/or clients once within the provider, then pass that information to resources by adding the `Configure` method. diff --git a/website/docs/plugin/framework/resources/create.mdx b/website/docs/plugin/framework/resources/create.mdx index db9ec0914..77951cd49 100644 --- a/website/docs/plugin/framework/resources/create.mdx +++ b/website/docs/plugin/framework/resources/create.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement resource creation in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Create resources Creation is part of the basic Terraform lifecycle for managing resources. During the [`terraform apply` command](/terraform/cli/commands/apply), Terraform calls the provider [`ApplyResourceChange`](/terraform/plugin/framework/internals/rpcs#applyresourcechange-rpc) RPC, in which the framework calls the [`resource.Resource` interface `Create` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Create). The request contains Terraform configuration and plan data. The response expects the applied Terraform state data, including any computed values. The data is defined by the [schema](/terraform/plugin/framework/data-handling/schemas) of the resource. diff --git a/website/docs/plugin/framework/resources/default.mdx b/website/docs/plugin/framework/resources/default.mdx index 5254d5c5b..cc697d138 100644 --- a/website/docs/plugin/framework/resources/default.mdx +++ b/website/docs/plugin/framework/resources/default.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Default values After [validation](/terraform/plugin/framework/validation) and before applying configuration changes, Terraform generates a plan that describes the expected values and behaviors of those changes. Resources can then tailor the plan to set default values on computed resource attributes that are null in the configuration. diff --git a/website/docs/plugin/framework/resources/delete.mdx b/website/docs/plugin/framework/resources/delete.mdx index 37c706e7f..d46cdd6b4 100644 --- a/website/docs/plugin/framework/resources/delete.mdx +++ b/website/docs/plugin/framework/resources/delete.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement resource deletion in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Delete resources Deletion is part of the basic Terraform lifecycle for managing resources. During the [`terraform apply` command](/terraform/cli/commands/apply), Terraform calls the provider [`ApplyResourceChange`](/terraform/plugin/framework/internals/rpcs#applyresourcechange-rpc) RPC, in which the framework calls the [`resource.Resource` interface `Delete` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Delete). The request contains Terraform prior state data. The response is only for returning diagnostics. The data is defined by the [schema](/terraform/plugin/framework/schemas) of the resource. diff --git a/website/docs/plugin/framework/resources/import.mdx b/website/docs/plugin/framework/resources/import.mdx index f8202686d..137391b4f 100644 --- a/website/docs/plugin/framework/resources/import.mdx +++ b/website/docs/plugin/framework/resources/import.mdx @@ -4,6 +4,9 @@ description: >- Learn how to support resource import using the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resource import Practitioners can use the [`terraform import` command](/terraform/cli/commands/import) to let Terraform begin managing existing infrastructure resources. Resources can implement the `ImportState` method, which must either specify enough Terraform state for the `Read` method to refresh [`resource.Resource`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource) or return an error. diff --git a/website/docs/plugin/framework/resources/index.mdx b/website/docs/plugin/framework/resources/index.mdx index 61d8b5798..fee87055f 100644 --- a/website/docs/plugin/framework/resources/index.mdx +++ b/website/docs/plugin/framework/resources/index.mdx @@ -5,6 +5,9 @@ description: >- allow Terraform to manage infrastructure objects. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Resources [Resources](/terraform/language/resources) are an abstraction that allow Terraform to manage infrastructure objects, such as a compute instance, an access policy, or disk. Terraform assumes that every resource: @@ -30,6 +33,7 @@ Further documentation is available for deeper resource concepts: - [Upgrade state](/terraform/plugin/framework/resources/state-upgrade) to transparently update state data outside plans. - [Validate](/terraform/plugin/framework/resources/validate-configuration) practitioner configuration against acceptable values. - [Timeouts](/terraform/plugin/framework/resources/timeouts) in practitioner configuration for use in resource create, read, update and delete functions. +- [Write-only Arguments](/terraform/plugin/framework/resources/write-only-arguments) are special types of attributes that can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. ## Define Resource Type diff --git a/website/docs/plugin/framework/resources/plan-modification.mdx b/website/docs/plugin/framework/resources/plan-modification.mdx index bcab40d8d..97eff9ce1 100644 --- a/website/docs/plugin/framework/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/resources/plan-modification.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Plan modification After [validation](/terraform/plugin/framework/validation) and before applying configuration changes, Terraform generates a plan that describes the expected values and behaviors of those changes. Resources can then tailor the plan to match the expected end state, prevent errant in-place updates, or return any [diagnostics](/terraform/plugin/framework/diagnostics). diff --git a/website/docs/plugin/framework/resources/private-state.mdx b/website/docs/plugin/framework/resources/private-state.mdx index 244b1a9b9..f813b74fa 100644 --- a/website/docs/plugin/framework/resources/private-state.mdx +++ b/website/docs/plugin/framework/resources/private-state.mdx @@ -5,6 +5,9 @@ description: >- Private state is provider-only data storage for resources. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Private state management Resource private state is provider maintained data that is stored in Terraform state alongside the schema-defined data. Private state is never accessed or exposed by Terraform plans, however providers can use this data storage for advanced use cases. diff --git a/website/docs/plugin/framework/resources/read.mdx b/website/docs/plugin/framework/resources/read.mdx index dc0161ae9..567986190 100644 --- a/website/docs/plugin/framework/resources/read.mdx +++ b/website/docs/plugin/framework/resources/read.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement resource read in the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Read resources Read (refresh) is part of the basic Terraform lifecycle for managing resources. During the [`terraform apply`](/terraform/cli/commands/apply), [`terraform plan`](/terraform/cli/commands/plan), and [`terraform refresh`](/terraform/cli/commands/refresh) commands, Terraform calls the provider [`ReadResource`](/terraform/plugin/framework/internals/rpcs#readresource-rpc) RPC, in which the framework calls the [`resource.Resource` interface `Read` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Read). The `Read` method is also executed after [resource import](/terraform/plugin/framework/resources/import). The request contains Terraform prior state data. The response contains the refreshed state data. The data is defined by the [schema](/terraform/plugin/framework/schemas) of the resource. diff --git a/website/docs/plugin/framework/resources/state-move.mdx b/website/docs/plugin/framework/resources/state-move.mdx index c97dc57cd..d7f8f58d8 100644 --- a/website/docs/plugin/framework/resources/state-move.mdx +++ b/website/docs/plugin/framework/resources/state-move.mdx @@ -5,6 +5,9 @@ description: >- the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # State move diff --git a/website/docs/plugin/framework/resources/state-upgrade.mdx b/website/docs/plugin/framework/resources/state-upgrade.mdx index 31fd5c678..bf34e7f46 100644 --- a/website/docs/plugin/framework/resources/state-upgrade.mdx +++ b/website/docs/plugin/framework/resources/state-upgrade.mdx @@ -5,6 +5,9 @@ description: >- one version of your Terraform framework provider to another. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # State upgrade A resource schema captures the structure and types of the resource [state](/terraform/language/state). Any state data that does not conform to the resource schema will generate errors or may not be persisted properly. Over time, it may be necessary for resources to make breaking changes to their schemas, such as changing an attribute type. Terraform supports versioning of these resource schemas and the current version is saved into the Terraform state. When the provider advertises a newer schema version, Terraform will call back to the provider to attempt to upgrade from the saved schema version to the one advertised. This operation is performed prior to planning, but with a configured provider. diff --git a/website/docs/plugin/framework/resources/timeouts.mdx b/website/docs/plugin/framework/resources/timeouts.mdx index db14bae0c..db22cd7f4 100644 --- a/website/docs/plugin/framework/resources/timeouts.mdx +++ b/website/docs/plugin/framework/resources/timeouts.mdx @@ -4,6 +4,9 @@ description: >- Learn how to implement timeouts with the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Timeouts The reality of cloud infrastructure is that it typically takes time to perform operations such as booting operating systems, discovering services, and replicating state across network edges. As the provider developer you should take known delays in resource APIs into account in the CRUD functions of the resource. Terraform supports configurable timeouts to assist in these situations. diff --git a/website/docs/plugin/framework/resources/update.mdx b/website/docs/plugin/framework/resources/update.mdx index a892b4161..3bf3f4de4 100644 --- a/website/docs/plugin/framework/resources/update.mdx +++ b/website/docs/plugin/framework/resources/update.mdx @@ -5,6 +5,9 @@ description: >- plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Update resources In-place update is part of the basic Terraform lifecycle for managing resources. During the [`terraform apply` command](/terraform/cli/commands/apply), Terraform calls the provider [`ApplyResourceChange`](/terraform/plugin/framework/internals/rpcs#applyresourcechange-rpc) RPC, in which the framework calls the [`resource.Resource` interface `Update` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Update). The request contains Terraform prior state, configuration, and plan data. The response contains updated state data. The data is defined by the [schema](/terraform/plugin/framework/schemas) of the resource. diff --git a/website/docs/plugin/framework/resources/validate-configuration.mdx b/website/docs/plugin/framework/resources/validate-configuration.mdx index 4a1d93661..27ab2dc17 100644 --- a/website/docs/plugin/framework/resources/validate-configuration.mdx +++ b/website/docs/plugin/framework/resources/validate-configuration.mdx @@ -5,6 +5,9 @@ description: >- framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Validate configuration [Resources](/terraform/plugin/framework/resources) support validating an entire practitioner configuration in either declarative or imperative logic. Feedback, such as required syntax or acceptable combinations of values, is returned via [diagnostics](/terraform/plugin/framework/diagnostics). diff --git a/website/docs/plugin/framework/resources/write-only-arguments.mdx b/website/docs/plugin/framework/resources/write-only-arguments.mdx new file mode 100644 index 000000000..c993a5755 --- /dev/null +++ b/website/docs/plugin/framework/resources/write-only-arguments.mdx @@ -0,0 +1,124 @@ +--- +page_title: 'Plugin Development - Framework: Write-only Arguments' +description: >- + How to implement write-only arguments with the provider development framework. +--- + +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + +# Write-only Arguments + +Write-only arguments are managed resource attributes that are configured by practitioners but are not persisted to the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. +Write-only arguments should be used to handle secret values that do not need to be persisted in Terraform state, such as passwords, API keys, etc. +The provider is expected to be the terminal point for an ephemeral value, +which should either use the value by making the appropriate change to the API or ignore the value. Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not required to be consistent between plan and apply operations. + +## General Concepts + +The following are high level differences between `Required`/`Optional` arguments and write-only arguments: + +- Write-only arguments can accept ephemeral and non-ephemeral values. + +- Write-only arguments cannot be used with set attributes, set nested attributes, and set nested blocks. + +- Write-only argument values are only available in the configuration. The prior state, planned state, and final state values for +write-only arguments should always be `null`. + - Provider developers do not need to explicitly set write-only argument values to `null` after using them as the plugin framework will handle the nullification of write-only arguments for all RPCs. + +- Any value that is set for a write-only argument in the state or plan (during [Plan Modification](/terraform/plugin/framework/resources/plan-modification)) by the provider will be reverted to `null` by plugin framework before the RPC response is sent to Terraform. + +- Write-only argument values cannot produce a Terraform plan difference. + - This is because the prior state value for a write-only argument will always be `null` and the planned/final state value will also be `null`, therefore, it cannot produce a diff on its own. + - The one exception to this case is if the write-only argument is added to `requires_replace` during Plan Modification (i.e., using the [`RequiresReplace()`](/terraform/plugin/framework/resources/plan-modification#requiresreplace) plan modifier), in that case, the write-only argument will always cause a diff/trigger a resource recreation. + +- Since write-only arguments can accept ephemeral values, write-only argument configuration values are not expected to be consistent between plan and apply. + +## Schema + +An attribute can be made write-only by setting the `WriteOnly` field to `true` in the schema. Attributes with `WriteOnly` set to `true` must also have +one of `Required` or `Optional` set to `true`. If a list nested, map nested, or single nested attribute has `WriteOnly` set to `true`, all child attributes must also have `WriteOnly` set to `true`. +A set nested block cannot have any child attributes with `WriteOnly` set to `true`. `Computed` cannot be set to true for write-only arguments. + +**Schema example:** + +```go +"password_wo": schema.StringAttribute{ + Required: true, + WriteOnly: true, +}, +``` + +## Retrieving Write-only Values + +Write-only argument values should be retrieved from the configuration instead of the plan. Refer to [accessing values](/terraform/plugin/framework/handling-data/accessing-values) for more details on +retrieving values from configuration. + +## PreferWriteOnlyAttribute Validators + + + + These validators will produce persistent warnings for practitioners on every Terraform run as long as the specified non-write-only attribute + has a value in the configuration. The validators will also produce warnings for users of shared modules who cannot immediately take action on the warning. + + + +The `PreferWriteOnlyAttribute()` validators available in the [`terraform-plugin-framework-validators` Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) +can be used when you have a write-only version of an existing attribute, and you want to encourage practitioners to use the write-only version whenever possible. + +The validator returns a warning if the Terraform client is 1.11 or above and the value of the existing attribute is non-null. + +`PreferWriteOnlyAttribute()` is available as a resource-level validator in the [`resourcevalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator) or +as an attribute-level validator in the `[type]validator` packages (i.e., [`stringvalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator)) + +Usage: + +```go +// Resource-level validator +// Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password"), + path.MatchRoot("password_wo"), + ), + } + +// Attribute-level validator +// Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "password": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value. + stringvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password_wo"), + ), + }, + }, + "password_wo": schema.StringAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +``` + +```hcl +resource "example_db_instance" "ex" { + username = "foo" + password = "bar" # returns a warning encouraging practitioners to use `password_wo` instead. +} +``` + +## Best Practices + +Since write-only arguments have no prior values, user intent or value changes cannot be determined with a write-only argument alone. To determine when to use/not use a write-only argument value in your provider, we recommend one of the following: + +- Pair write-only arguments with a configuration attribute (required or optional) to “trigger” the use of the write-only argument + - For example, a `password_wo` write-only argument can be paired with a configured `password_wo_version` attribute. When the `password_wo_version` is modified, the provider will send the `password_wo` value to the API. +- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value. +- Use the resource's [private state](/terraform/plugin/framework/resources/private-state) to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs. \ No newline at end of file diff --git a/website/docs/plugin/framework/validation.mdx b/website/docs/plugin/framework/validation.mdx index fedfbafb9..2daf5bd0b 100644 --- a/website/docs/plugin/framework/validation.mdx +++ b/website/docs/plugin/framework/validation.mdx @@ -4,6 +4,9 @@ description: >- Learn how to validate configuration values using the Terraform plugin framework. --- +> [!IMPORTANT] +> **Documentation Update:** Product documentation previously located in `/website` has moved to the [`hashicorp/web-unified-docs`](https://github.com/hashicorp/web-unified-docs) repository, where all product documentation is now centralized. Please make contributions directly to `web-unified-docs`, since changes to `/website` in this repository will not appear on developer.hashicorp.com. + # Validation The framework can return [diagnostics](/terraform/plugin/framework/diagnostics) feedback for values in provider, resource, and data source configurations or [errors](/terraform/plugin/framework/functions/errors) feedback for values in function parameters. This allows you to write validations that give users feedback about required syntax, types, and acceptable values.