diff --git a/.changes/1.12.0.md b/.changes/1.12.0.md new file mode 100644 index 000000000..249d40565 --- /dev/null +++ b/.changes/1.12.0.md @@ -0,0 +1,10 @@ +## 1.12.0 (September 18, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors ([#1033](https://github.com/hashicorp/terraform-plugin-framework/issues/1033)) + +BUG FIXES: + +* providerserver: Fixed bug that prevented `moved` operation support between resource types for framework-only providers. ([#1039](https://github.com/hashicorp/terraform-plugin-framework/issues/1039)) + diff --git a/.changes/1.13.0.md b/.changes/1.13.0.md new file mode 100644 index 000000000..f15d176a5 --- /dev/null +++ b/.changes/1.13.0.md @@ -0,0 +1,17 @@ +## 1.13.0 (October 31, 2024) + +NOTES: + +* Ephemeral resource support is in technical preview and offered without compatibility promises until Terraform 1.10 is generally available. ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + +FEATURES: + +* ephemeral: New package for implementing ephemeral resources ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* ephemeral/schema: New package for implementing ephemeral resource schemas ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + +ENHANCEMENTS: + +* provider: Added `ProviderWithEphemeralResources` interface for implementing ephemeral resources ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* tfsdk: Added `EphemeralResultData` struct for representing ephemeral values produced by a provider, such as from an ephemeral resource ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* provider: Added `EphemeralResourceData` to `ConfigureResponse`, to pass provider-defined data to `ephemeral.EphemeralResource` implementations ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + 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/.github/CODEOWNERS b/.github/CODEOWNERS index 922ee27f4..36958c1cc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,17 @@ * @hashicorp/terraform-devex + +# 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/ + +# 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-changie.yml b/.github/workflows/ci-changie.yml index de5554dda..3031aad83 100644 --- a/.github/workflows/ci-changie.yml +++ b/.github/workflows/ci-changie.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: # Ensure terraform-devex-repos is updated on version changes. - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Ensure terraform-devex-repos is updated on version changes. - uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 with: diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index ce2d45e52..8451dde35 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,8 +13,8 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.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 5817291dd..9934244e9 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,12 +16,12 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 + - uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1 terraform-provider-corner-tfprotov5: defaults: run: @@ -29,15 +29,15 @@ jobs: name: terraform-provider-corner (tfprotov5 / Terraform ${{ matrix.terraform}}) runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: 'go.mod' - - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: terraform_version: ${{ matrix.terraform }} terraform_wrapper: false @@ -55,15 +55,15 @@ jobs: name: terraform-provider-corner (tfprotov6 / Terraform ${{ matrix.terraform}}) runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: 'go.mod' - - uses: hashicorp/setup-terraform@651471c36a6092792c552e8b1bef71e592b462d8 # v3.1.1 + - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: terraform_version: ${{ matrix.terraform }} terraform_wrapper: false @@ -79,16 +79,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.22', '1.21' ] + go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.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@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 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 0603e6b35..2c62e130b 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,10 +14,10 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + - uses: goreleaser/goreleaser-action@026299872805cb2db698e02dd7fb506a4da5122d # v6.2.0 with: args: check diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index f745aa05d..d640beb48 100644 --- a/.github/workflows/compliance.yml +++ b/.github/workflows/compliance.yml @@ -11,7 +11,7 @@ jobs: copywrite: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: hashicorp/setup-copywrite@32638da2d4e81d56a0764aa1547882fc4d209636 # v1.1.3 - run: copywrite headers --plan - run: copywrite license --plan \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 054217462..14c71529e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Default input is the SHA that initially triggered the workflow. As we created a new commit in the previous job, @@ -79,12 +79,12 @@ jobs: contents: write # Needed for goreleaser to create GitHub release issues: write # Needed for goreleaser to close associated milestone steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.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@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + - uses: goreleaser/goreleaser-action@026299872805cb2db698e02dd7fb506a4da5122d # v6.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.golangci.yml b/.golangci.yml index a62a62740..1e7c56bf6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,16 +1,17 @@ issues: - max-per-linter: 0 + max-issues-per-linter: 0 max-same-issues: 0 linters: disable-all: true enable: + - copyloopvar - durationcheck - errcheck - - exportloopref - forcetypeassert - gofmt - gosimple + - govet - ineffassign - makezero - misspell @@ -18,11 +19,10 @@ linters: - paralleltest - predeclared - staticcheck - - tenv - unconvert - unparam - unused - - vet + - usetesting run: # Prevent false positive timeouts in CI diff --git a/CHANGELOG.md b/CHANGELOG.md index 1920a5e6b..7e7c35c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +## 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: + +* Ephemeral resource support is in technical preview and offered without compatibility promises until Terraform 1.10 is generally available. ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + +FEATURES: + +* ephemeral: New package for implementing ephemeral resources ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* ephemeral/schema: New package for implementing ephemeral resource schemas ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + +ENHANCEMENTS: + +* provider: Added `ProviderWithEphemeralResources` interface for implementing ephemeral resources ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* tfsdk: Added `EphemeralResultData` struct for representing ephemeral values produced by a provider, such as from an ephemeral resource ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) +* provider: Added `EphemeralResourceData` to `ConfigureResponse`, to pass provider-defined data to `ephemeral.EphemeralResource` implementations ([#1050](https://github.com/hashicorp/terraform-plugin-framework/issues/1050)) + +## 1.12.0 (September 18, 2024) + +NOTES: + +* all: This Go module has been updated to Go 1.22 per the [Go support policy](https://go.dev/doc/devel/release#policy). It is recommended to review the [Go 1.22 release notes](https://go.dev/doc/go1.22) before upgrading. Any consumers building on earlier Go versions may experience errors ([#1033](https://github.com/hashicorp/terraform-plugin-framework/issues/1033)) + +BUG FIXES: + +* providerserver: Fixed bug that prevented `moved` operation support between resource types for framework-only providers. ([#1039](https://github.com/hashicorp/terraform-plugin-framework/issues/1039)) + ## 1.11.0 (August 06, 2024) NOTES: diff --git a/README.md b/README.md index 6e2ba9d97..294ba5ead 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.21** or later must be used when including this project as a dependency. +Currently, that means Go **1.22** or later must be used when including this project as a dependency. ## Contributing diff --git a/datasource/schema/attribute.go b/datasource/schema/attribute.go index 4a0feceec..67294bfc6 100644 --- a/datasource/schema/attribute.go +++ b/datasource/schema/attribute.go @@ -10,7 +10,10 @@ import ( // Attribute define a value field inside the Schema. Implementations in this // package include: // - BoolAttribute +// - DynamicAttribute +// - Float32Attribute // - Float64Attribute +// - Int32Attribute // - Int64Attribute // - ListAttribute // - MapAttribute diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index b9f6e3820..1d984a8db 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,6 +182,11 @@ 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 diff --git a/datasource/schema/bool_attribute_test.go b/datasource/schema/bool_attribute_test.go index 40e36a08d..ea22a21fc 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,29 @@ 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) + } + }) + } +} diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 6b1b6c83e..4659cadf6 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,11 @@ 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 +} + // 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..7166ae9cb 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() diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index 8f3dbdc21..d2510f5c3 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -189,3 +189,8 @@ 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 +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go index 0da3cc94e..3149803e0 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,29 @@ 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) + } + }) + } +} diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 1313353ec..1d893dd33 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,8 @@ 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 +} diff --git a/datasource/schema/float64_attribute_test.go b/datasource/schema/float64_attribute_test.go index a70ca695b..c73193387 100644 --- a/datasource/schema/float64_attribute_test.go +++ b/datasource/schema/float64_attribute_test.go @@ -9,13 +9,15 @@ 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" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { @@ -54,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() @@ -102,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() @@ -136,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() @@ -171,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() @@ -205,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() @@ -239,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() @@ -264,17 +254,15 @@ func TestFloat64AttributeGetType(t *testing.T) { attribute: schema.Float64Attribute{}, expected: types.Float64Type, }, - // "custom-type": { - // attribute: schema.Float64Attribute{ - // CustomType: testtypes.Float64Type{}, - // }, - // expected: testtypes.Float64Type{}, - // }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -307,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() @@ -341,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() @@ -375,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() @@ -409,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() @@ -422,3 +402,29 @@ 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) + } + }) + } +} diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index 89f852e8c..d06a9b8ce 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -189,3 +189,8 @@ 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 +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go index 889e3686d..15404c9e8 100644 --- a/datasource/schema/int32_attribute_test.go +++ b/datasource/schema/int32_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -55,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() @@ -103,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() @@ -138,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() @@ -172,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() @@ -206,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() @@ -231,17 +222,15 @@ func TestInt32AttributeGetType(t *testing.T) { attribute: schema.Int32Attribute{}, expected: types.Int32Type, }, - // "custom-type": { - // attribute: schema.Int32Attribute{ - // CustomType: testtypes.Int32Type{}, - // }, - // expected: testtypes.Int32Type{}, - // }, + "custom-type": { + attribute: schema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -274,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() @@ -308,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() @@ -342,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() @@ -376,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() @@ -410,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() @@ -423,3 +402,29 @@ 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) + } + }) + } +} diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index ab9d5ca1b..85bd4a445 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,8 @@ 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 +} diff --git a/datasource/schema/int64_attribute_test.go b/datasource/schema/int64_attribute_test.go index 7d560560a..ed0544c62 100644 --- a/datasource/schema/int64_attribute_test.go +++ b/datasource/schema/int64_attribute_test.go @@ -9,13 +9,15 @@ 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" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { @@ -54,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() @@ -102,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() @@ -137,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() @@ -171,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() @@ -205,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() @@ -230,17 +222,15 @@ func TestInt64AttributeGetType(t *testing.T) { attribute: schema.Int64Attribute{}, expected: types.Int64Type, }, - // "custom-type": { - // attribute: schema.Int64Attribute{ - // CustomType: testtypes.Int64Type{}, - // }, - // expected: testtypes.Int64Type{}, - // }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -273,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() @@ -307,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() @@ -341,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() @@ -375,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() @@ -409,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() @@ -422,3 +402,29 @@ 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) + } + }) + } +} diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 9d502067f..2e5140602 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,6 +203,11 @@ 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 diff --git a/datasource/schema/list_attribute_test.go b/datasource/schema/list_attribute_test.go index 5c33f4fbe..c38c41e2f 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() @@ -239,17 +230,15 @@ func TestListAttributeGetType(t *testing.T) { attribute: schema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index b9b70d6fb..922320627 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,11 @@ 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 +} + // 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 5fb035b29..b7ea5be20 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() @@ -386,17 +375,15 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/list_nested_block_test.go b/datasource/schema/list_nested_block_test.go index b1ac538b7..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() @@ -467,17 +453,15 @@ func TestListNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.ListNestedBlock{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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 d9b701f73..3d6c57680 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. @@ -23,7 +24,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapAttribute{} ) -// MapAttribute represents a schema attribute that is a list with a single +// MapAttribute represents a schema attribute that is a map with a single // element type. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The ElementType field // must be set. @@ -32,7 +33,7 @@ var ( // require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a list or directly via curly brace syntax. +// return a map or directly via curly brace syntax. // // # map of strings // example_attribute = { @@ -205,6 +206,11 @@ 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 +} + // 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 6abffa4bb..af361a636 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() @@ -239,17 +230,15 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index 2f3a60ec5..9bf1bb957 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -25,7 +25,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} ) -// MapNestedAttribute represents an attribute that is a set of objects where +// MapNestedAttribute represents an attribute that is a map of objects where // the object attributes can be fully defined, including further nested // attributes. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The NestedObject field @@ -35,7 +35,7 @@ var ( // not require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a set of objects or directly via curly brace syntax. +// return a map of objects or directly via curly brace syntax. // // # map of objects // example_attribute = { @@ -195,7 +195,7 @@ func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeMap. func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeMap } @@ -231,6 +231,11 @@ 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 +} + // 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 7e6dbc2a0..d691cda82 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() @@ -386,17 +375,15 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/nested_attribute_object_test.go b/datasource/schema/nested_attribute_object_test.go index 2708eddf2..74bf24506 100644 --- a/datasource/schema/nested_attribute_object_test.go +++ b/datasource/schema/nested_attribute_object_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "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" @@ -79,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() @@ -144,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() @@ -184,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() @@ -222,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() @@ -255,17 +248,15 @@ func TestNestedAttributeObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedAttributeObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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 a5631f4cb..550b8563f 100644 --- a/datasource/schema/nested_block_object_test.go +++ b/datasource/schema/nested_block_object_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "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" @@ -97,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() @@ -162,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() @@ -202,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() @@ -258,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() @@ -296,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() @@ -341,17 +332,15 @@ func TestNestedBlockObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedBlockObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..c21f74a15 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,11 @@ 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 +} + // 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..c30b711eb 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() diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index eafa40c6e..a004329ac 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,11 @@ 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 +} + // 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 e6139dc13..d5c45ee64 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() @@ -245,17 +236,15 @@ func TestObjectAttributeGetType(t *testing.T) { attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, }, - // "custom-type": { - // attribute: schema.ObjectAttribute{ - // CustomType: testtypes.ObjectType{}, - // }, - // expected: testtypes.ObjectType{}, - // }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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() 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..859b0558d 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,11 @@ 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 +} + // 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 4d8f3c3f9..02fcb830c 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() @@ -239,17 +230,15 @@ func TestSetAttributeGetType(t *testing.T) { attribute: schema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 860ab4c96..26b14e449 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -190,7 +190,7 @@ func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSet. func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSet } @@ -226,6 +226,11 @@ 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 +} + // 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 630a64863..1a50cac03 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() @@ -386,17 +375,15 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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() diff --git a/datasource/schema/set_nested_block_test.go b/datasource/schema/set_nested_block_test.go index 988aee797..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() @@ -467,17 +453,15 @@ func TestSetNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SetNestedBlock{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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 21c9f3231..b6f4e7f32 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -198,7 +198,7 @@ func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject } } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSingle. func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSingle } @@ -240,6 +240,11 @@ 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 +} + // 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 817eaaf7c..0a7a0b66f 100644 --- a/datasource/schema/single_nested_attribute_test.go +++ b/datasource/schema/single_nested_attribute_test.go @@ -9,13 +9,15 @@ 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" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { @@ -80,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() @@ -171,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() @@ -209,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() @@ -247,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() @@ -285,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() @@ -321,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() @@ -354,17 +344,15 @@ func TestSingleNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SingleNestedAttribute{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -401,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() @@ -439,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() @@ -477,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() @@ -515,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() @@ -529,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() @@ -553,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() diff --git a/datasource/schema/single_nested_block_test.go b/datasource/schema/single_nested_block_test.go index e204b534c..0208eb561 100644 --- a/datasource/schema/single_nested_block_test.go +++ b/datasource/schema/single_nested_block_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -98,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() @@ -150,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() @@ -252,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() @@ -290,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() @@ -328,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() @@ -378,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() @@ -416,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() @@ -461,17 +448,15 @@ func TestSingleNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SingleNestedBlock{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + block: schema.SingleNestedBlock{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..95534fece 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,11 @@ 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 +} + // 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..09ea194f2 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() 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/close.go b/ephemeral/close.go new file mode 100644 index 000000000..b53096701 --- /dev/null +++ b/ephemeral/close.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +// CloseRequest represents a request for the provider to close an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Close function. +type CloseRequest struct { + // Private is provider-defined ephemeral resource private state data + // which was previously provided by the latest Open or Renew operation. + // + // Use the GetKey method to read data. + Private *privatestate.ProviderData +} + +// CloseResponse represents a response to a CloseRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Close function, in which the provider +// should set values on the CloseResponse as appropriate. +type CloseResponse struct { + // Diagnostics report errors or warnings related to closing the + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/config_validator.go b/ephemeral/config_validator.go new file mode 100644 index 000000000..1e8b15c7a --- /dev/null +++ b/ephemeral/config_validator.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import "context" + +// ConfigValidator describes reusable EphemeralResource configuration validation functionality. +type ConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to ephemeral resource plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to ephemeral resource Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // ValidateEphemeralResource performs the validation. + // + // This method name is separate from the datasource.ConfigValidator + // interface ValidateDataSource method name, provider.ConfigValidator + // interface ValidateProvider method name, and resource.ConfigValidator + // interface ValidateResource method name to allow generic validators. + ValidateEphemeralResource(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/ephemeral/configure.go b/ephemeral/configure.go new file mode 100644 index 000000000..d815dfcd0 --- /dev/null +++ b/ephemeral/configure.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// ConfigureRequest represents a request for the provider to configure an +// ephemeral resource, i.e., set provider-level data or clients. An instance of +// this request struct is supplied as an argument to the EphemeralResource type +// Configure method. +type ConfigureRequest struct { + // ProviderData is the data set in the + // [provider.ConfigureResponse.EphemeralResourceData] field. This data is + // provider-specifc and therefore can contain any necessary remote system + // clients, custom provider data, or anything else pertinent to the + // functionality of the EphemeralResource. + // + // This data is only set after the ConfigureProvider RPC has been called + // by Terraform. + ProviderData any +} + +// ConfigureResponse represents a response to a ConfigureRequest. An +// instance of this response struct is supplied as an argument to the +// EphemeralResource type Configure method. +type ConfigureResponse struct { + // Diagnostics report errors or warnings related to configuring of the + // EphemeralResource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/deferred.go b/ephemeral/deferred.go new file mode 100644 index 000000000..d4773a10a --- /dev/null +++ b/ephemeral/deferred.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonEphemeralResourceConfigUnknown is used to indicate that the resource configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonEphemeralResourceConfigUnknown DeferredReason = 1 + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. + DeferredReasonProviderConfigUnknown DeferredReason = 2 + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. + DeferredReasonAbsentPrereq DeferredReason = 3 +) + +// Deferred is used to indicate to Terraform that a change needs to be deferred for a reason. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type Deferred struct { + // Reason is the reason for deferring the change. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 1: + return "Ephemeral Resource Config Unknown" + case 2: + return "Provider Config Unknown" + case 3: + return "Absent Prerequisite" + } + return "Unknown" +} diff --git a/ephemeral/doc.go b/ephemeral/doc.go new file mode 100644 index 000000000..02b0e6e3a --- /dev/null +++ b/ephemeral/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package ephemeral contains all interfaces, request types, and response +// types for an ephemeral resource implementation. +// +// In Terraform, an ephemeral resource is a concept which enables provider +// developers to offer practitioners ephemeral values, which will not be stored +// in any artifact produced by Terraform (plan/state). Ephemeral resources can +// optionally implement renewal logic via the (EphemeralResource).Renew method +// and cleanup logic via the (EphemeralResource).Close method. +// +// Ephemeral resources are not saved into the Terraform plan or state and can +// only be referenced in other ephemeral values, such as provider configuration +// attributes. Ephemeral resources are defined by a type/name, such as "examplecloud_thing", +// a schema representing the structure and data types of configuration, and lifecycle logic. +// +// The main starting point for implementations in this package is the +// EphemeralResource type which represents an instance of an ephemeral resource +// 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. +package ephemeral diff --git a/ephemeral/ephemeral_resource.go b/ephemeral/ephemeral_resource.go new file mode 100644 index 000000000..b15707f3b --- /dev/null +++ b/ephemeral/ephemeral_resource.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "context" +) + +// EphemeralResource represents an instance of an ephemeral resource type. This is the core +// interface that all ephemeral resources must implement. +// +// Ephemeral resources can optionally implement these additional concepts: +// +// - Configure: Include provider-level data or clients via EphemeralResourceWithConfigure +// +// - Validation: Schema-based or entire configuration via EphemeralResourceWithConfigValidators +// or EphemeralResourceWithValidateConfig. +// +// - Renew: Handle renewal of an expired remote object via EphemeralResourceWithRenew. +// Ephemeral resources can indicate to Terraform when a renewal must occur via the RenewAt +// response field of the Open/Renew methods. Renew cannot return new result data for the +// ephemeral resource instance, so this logic is only appropriate for remote objects like +// HashiCorp Vault leases, which can be renewed without changing their data. +// +// - Close: Allows providers to clean up the ephemeral resource via EphemeralResourceWithClose. +type EphemeralResource interface { + // Metadata should return the full name of the ephemeral resource, such as + // examplecloud_thing. + Metadata(context.Context, MetadataRequest, *MetadataResponse) + + // Schema should return the schema for this ephemeral resource. + Schema(context.Context, SchemaRequest, *SchemaResponse) + + // Open is called when the provider must generate a new ephemeral resource. Config values + // should be read from the OpenRequest and new response values set on the OpenResponse. + Open(context.Context, OpenRequest, *OpenResponse) +} + +// EphemeralResourceWithRenew is an interface type that extends EphemeralResource to +// include a method which the framework will call when Terraform detects that the +// provider-defined returned RenewAt time for an ephemeral resource has passed. This RenewAt +// response field can be set in the OpenResponse and RenewResponse. +type EphemeralResourceWithRenew interface { + EphemeralResource + + // Renew is called when the provider must renew the ephemeral resource based on + // the provided RenewAt time. This RenewAt response field can be set in the OpenResponse and RenewResponse. + // + // Renew cannot return new result data for the ephemeral resource instance, so this logic is only appropriate + // for remote objects like HashiCorp Vault leases, which can be renewed without changing their data. + Renew(context.Context, RenewRequest, *RenewResponse) +} + +// EphemeralResourceWithClose is an interface type that extends +// EphemeralResource to include a method which the framework will call when +// Terraform determines that the ephemeral resource can be safely cleaned up. +type EphemeralResourceWithClose interface { + EphemeralResource + + // Close is called when the provider can clean up the ephemeral resource. + // Config values may be read from the CloseRequest. + Close(context.Context, CloseRequest, *CloseResponse) +} + +// EphemeralResourceWithConfigure is an interface type that extends EphemeralResource to +// include a method which the framework will automatically call so provider +// developers have the opportunity to setup any necessary provider-level data +// or clients in the EphemeralResource type. +type EphemeralResourceWithConfigure interface { + EphemeralResource + + // Configure enables provider-level data or clients to be set in the + // provider-defined EphemeralResource type. + Configure(context.Context, ConfigureRequest, *ConfigureResponse) +} + +// EphemeralResourceWithConfigValidators is an interface type that extends EphemeralResource to include declarative validations. +// +// Declaring validation using this methodology simplifies implementation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type EphemeralResourceWithConfigValidators interface { + EphemeralResource + + // ConfigValidators returns a list of functions which will all be performed during validation. + ConfigValidators(context.Context) []ConfigValidator +} + +// EphemeralResourceWithValidateConfig is an interface type that extends EphemeralResource to include imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single ephemeral resource. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ConfigValidators and ValidateConfig, if both are +// implemented, in addition to any Attribute or Type validation. +type EphemeralResourceWithValidateConfig interface { + EphemeralResource + + // ValidateConfig performs the validation. + ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/ephemeral/metadata.go b/ephemeral/metadata.go new file mode 100644 index 000000000..35ccbc0f9 --- /dev/null +++ b/ephemeral/metadata.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +// MetadataRequest represents a request for the EphemeralResource to return metadata, +// such as its type name. An instance of this request struct is supplied as +// an argument to the EphemeralResource type Metadata method. +type MetadataRequest struct { + // ProviderTypeName is the string returned from + // [provider.MetadataResponse.TypeName], if the Provider type implements + // the Metadata method. This string should prefix the EphemeralResource type name + // with an underscore in the response. + ProviderTypeName string +} + +// MetadataResponse represents a response to a MetadataRequest. An +// instance of this response struct is supplied as an argument to the +// EphemeralResource type Metadata method. +type MetadataResponse struct { + // TypeName should be the full ephemeral resource type, including the provider + // type prefix and an underscore. For example, examplecloud_thing. + TypeName string +} diff --git a/ephemeral/open.go b/ephemeral/open.go new file mode 100644 index 000000000..591222c59 --- /dev/null +++ b/ephemeral/open.go @@ -0,0 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// OpenClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the OpenEphemeralResource RPC, +// such as forward-compatible Terraform behavior changes. +type OpenClientCapabilities struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the request allows a deferral response. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + DeferralAllowed bool +} + +// OpenRequest represents a request for the provider to open an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Open function. +type OpenRequest struct { + // Config is the configuration the user supplied for the ephemeral + // resource. + Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for the + // OpenEphemeralResource RPC, such as forward-compatible Terraform behavior changes. + ClientCapabilities OpenClientCapabilities +} + +// OpenResponse represents a response to a OpenRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Open function, in which the provider +// should set values on the OpenResponse as appropriate. +type OpenResponse struct { + // Result is the object representing the values of the ephemeral + // resource following the Open operation. This field is pre-populated + // from OpenRequest.Config and should be set during the resource's Open + // operation. + Result tfsdk.EphemeralResultData + + // Private is the private state ephemeral resource data following the + // Open operation. This field is not pre-populated as there is no + // pre-existing private state data during the ephemeral resource's + // Open operation. + // + // This private data will be passed to any Renew or Close operations. + Private *privatestate.ProviderData + + // RenewAt is an optional date/time field that indicates to Terraform + // when this ephemeral resource must be renewed at. Terraform will call + // the (EphemeralResource).Renew method when the current date/time is on + // or after RenewAt during a Terraform operation. + // + // It is recommended to add extra time (usually no more than a few minutes) + // before an ephemeral resource expires to account for latency. + RenewAt time.Time + + // Diagnostics report errors or warnings related to opening the ephemeral + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics + + // Deferred indicates that Terraform should defer opening this + // ephemeral resource until a followup apply operation. + // + // This field can only be set if + // `(ephemeral.OpenRequest).ClientCapabilities.DeferralAllowed` is true. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + Deferred *Deferred +} diff --git a/ephemeral/renew.go b/ephemeral/renew.go new file mode 100644 index 000000000..6a32da449 --- /dev/null +++ b/ephemeral/renew.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +// RenewRequest represents a request for the provider to renew an ephemeral +// resource. An instance of this request struct is supplied as an argument to +// the ephemeral resource's Renew function. +type RenewRequest struct { + // Private is provider-defined ephemeral resource private state data + // which was previously provided by the latest Open or Renew operation. + // Any existing data is copied to RenewResponse.Private to prevent + // accidental private state data loss. + // + // Use the GetKey method to read data. Use the SetKey method on + // RenewResponse.Private to update or remove a value. + Private *privatestate.ProviderData +} + +// RenewResponse represents a response to a RenewRequest. An +// instance of this response struct is supplied as an argument +// to the ephemeral resource's Renew function, in which the provider +// should set values on the RenewResponse as appropriate. +type RenewResponse struct { + // RenewAt is an optional date/time field that indicates to Terraform + // when this ephemeral resource must be renewed at. Terraform will call + // the (EphemeralResource).Renew method when the current date/time is on + // or after RenewAt during a Terraform operation. + // + // It is recommended to add extra time (usually no more than a few minutes) + // before an ephemeral resource expires to account for latency. + RenewAt time.Time + + // Private is the private state ephemeral resource data following the + // Renew operation. This field is pre-populated from RenewRequest.Private + // and can be modified during the ephemeral resource's Renew operation. + // + // This private data will be passed to any Renew or Close operations. + Private *privatestate.ProviderData + + // Diagnostics report errors or warnings related to renewing the ephemeral + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/schema.go b/ephemeral/schema.go new file mode 100644 index 000000000..c9cd56d0b --- /dev/null +++ b/ephemeral/schema.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" +) + +// SchemaRequest represents a request for the EphemeralResource to return its schema. +// An instance of this request struct is supplied as an argument to the +// EphemeralResource type Schema method. +type SchemaRequest struct{} + +// SchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the EphemeralResource type Schema +// method. +type SchemaResponse struct { + // Schema is the schema of the ephemeral resource. + Schema schema.Schema + + // Diagnostics report errors or warnings related to retrieving the ephemeral + // resource schema. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/ephemeral/schema/attribute.go b/ephemeral/schema/attribute.go new file mode 100644 index 000000000..8b6ebe60b --- /dev/null +++ b/ephemeral/schema/attribute.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute defines a value field inside the Schema. Implementations in this +// package include: +// - BoolAttribute +// - DynamicAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - MapAttribute +// - NumberAttribute +// - ObjectAttribute +// - SetAttribute +// - StringAttribute +// +// Additionally, the NestedAttribute interface extends Attribute with nested +// attributes. Only supported in protocol version 6. Implementations in this +// package include: +// - ListNestedAttribute +// - MapNestedAttribute +// - SetNestedAttribute +// - SingleNestedAttribute +// +// In practitioner configurations, an equals sign (=) is required to set +// the value. [Configuration Reference] +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Attribute interface { + fwschema.Attribute +} diff --git a/ephemeral/schema/block.go b/ephemeral/schema/block.go new file mode 100644 index 000000000..f741d8f8e --- /dev/null +++ b/ephemeral/schema/block.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Block defines a structural field inside a Schema. Implementations in this +// package include: +// - ListNestedBlock +// - SetNestedBlock +// - SingleNestedBlock +// +// In practitioner configurations, an equals sign (=) cannot be used to set the +// value. Blocks are instead repeated as necessary, or require the use of +// [Dynamic Block Expressions]. +// +// Prefer NestedAttribute over Block. Blocks should typically be used for +// configuration compatibility with previously existing schemas from an older +// Terraform Plugin SDK. Efforts should be made to convert from Block to +// NestedAttribute as a breaking change for practitioners. +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Block interface { + fwschema.Block +} diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go new file mode 100644 index 000000000..56790dee2 --- /dev/null +++ b/ephemeral/schema/bool_attribute.go @@ -0,0 +1,192 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} + _ fwxschema.AttributeWithBoolValidators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Bool +} + +// 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) +} + +// BoolValidators returns the Validators field value. +func (a BoolAttribute) BoolValidators() []validator.Bool { + return a.Validators +} + +// 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 the DeprecationMessage field value. +func (a BoolAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a BoolAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a BoolAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a BoolAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go new file mode 100644 index 000000000..30cc54aa2 --- /dev/null +++ b/ephemeral/schema/bool_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: schema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: schema.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 TestBoolAttributeBoolValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected []validator.Bool + }{ + "no-validators": { + attribute: schema.BoolAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.BoolAttribute{ + Validators: []validator.Bool{}, + }, + expected: []validator.Bool{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.BoolValidators() + + 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 schema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.BoolAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: schema.BoolAttribute{}, + other: schema.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 schema.BoolAttribute + expected string + }{ + "no-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.BoolAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.BoolAttribute{ + MarkdownDescription: "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 schema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: schema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: schema.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 schema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.BoolAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.BoolAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "required": { + attribute: schema.BoolAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.BoolAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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) + } + }) + } +} diff --git a/ephemeral/schema/doc.go b/ephemeral/schema/doc.go new file mode 100644 index 000000000..12f7c3362 --- /dev/null +++ b/ephemeral/schema/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package schema contains all available schema functionality for ephemeral resources. +// Ephemeral resource schemas define the structure and value types for configuration +// and result data. Schemas are implemented via the ephemeral.EphemeralResource type +// Schema method. +package schema diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go new file mode 100644 index 000000000..9cd22e70a --- /dev/null +++ b/ephemeral/schema/dynamic_attribute.go @@ -0,0 +1,193 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = DynamicAttribute{} + _ fwxschema.AttributeWithDynamicValidators = DynamicAttribute{} +) + +// DynamicAttribute represents a schema attribute that is a dynamic, rather +// than a single static type. Static types are always preferable over dynamic +// types in Terraform as practitioners will receive less helpful configuration +// assistance from validation error diagnostics and editor integrations. When +// retrieving the value for this attribute, use types.Dynamic as the value type +// unless the CustomType field is set. +// +// The concrete value type for a dynamic is determined at runtime in this order: +// 1. By Terraform, if defined in the configuration (if Required or Optional). +// 2. By the provider (if Computed). +// +// Once the concrete value type has been determined, it must remain consistent between +// plan and apply or Terraform will return an error. +type DynamicAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.DynamicType. When retrieving data, the basetypes.DynamicValuable + // associated with this custom type must be used in place of types.Dynamic. + CustomType basetypes.DynamicTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Dynamic +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a DynamicAttribute. +func (a DynamicAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a DynamicAttribute +// and all fields are equal. +func (a DynamicAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(DynamicAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a DynamicAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a DynamicAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a DynamicAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.DynamicType or the CustomType field value if defined. +func (a DynamicAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.DynamicType +} + +// IsComputed returns the Computed field value. +func (a DynamicAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a DynamicAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a DynamicAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go new file mode 100644 index 000000000..4d01e95a0 --- /dev/null +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -0,0 +1,429 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.DynamicAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.DynamicType"), + }, + "ElementKeyInt": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.DynamicType"), + }, + "ElementKeyString": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.DynamicType"), + }, + "ElementKeyValue": { + attribute: schema.DynamicAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.DynamicType"), + }, + } + + 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 TestDynamicAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.DynamicAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestDynamicAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.DynamicAttribute{}, + other: testschema.AttributeWithDynamicValidators{}, + expected: false, + }, + "equal": { + attribute: schema.DynamicAttribute{}, + other: schema.DynamicAttribute{}, + 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 TestDynamicAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "description": { + attribute: schema.DynamicAttribute{ + 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 TestDynamicAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.DynamicAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.DynamicAttribute{ + MarkdownDescription: "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 TestDynamicAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected attr.Type + }{ + "base": { + attribute: schema.DynamicAttribute{}, + expected: types.DynamicType, + }, + "custom-type": { + attribute: schema.DynamicAttribute{ + CustomType: testtypes.DynamicType{}, + }, + expected: testtypes.DynamicType{}, + }, + } + + 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 TestDynamicAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-computed": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.DynamicAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestDynamicAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optional": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.DynamicAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestDynamicAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-required": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "required": { + attribute: schema.DynamicAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestDynamicAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.DynamicAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected []validator.Dynamic + }{ + "no-validators": { + attribute: schema.DynamicAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.DynamicAttribute{ + Validators: []validator.Dynamic{}, + }, + expected: []validator.Dynamic{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.DynamicValidators() + + 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 new file mode 100644 index 000000000..46af5bae2 --- /dev/null +++ b/ephemeral/schema/float32_attribute.go @@ -0,0 +1,195 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} + _ fwxschema.AttributeWithFloat32Validators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float32 +} + +// 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) +} + +// Float32Validators returns the Validators field value. +func (a Float32Attribute) Float32Validators() []validator.Float32 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a Float32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go new file mode 100644 index 000000000..1617d12af --- /dev/null +++ b/ephemeral/schema/float32_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: schema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: schema.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 TestFloat32AttributeFloat32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected []validator.Float32 + }{ + "no-validators": { + attribute: schema.Float32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float32Attribute{ + Validators: []validator.Float32{}, + }, + expected: []validator.Float32{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float32Validators() + + 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 schema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float32Attribute{}, + other: schema.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 schema.Float32Attribute + expected string + }{ + "no-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float32Attribute{ + MarkdownDescription: "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 schema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: schema.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 schema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float32Attribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go new file mode 100644 index 000000000..f4699e210 --- /dev/null +++ b/ephemeral/schema/float64_attribute.go @@ -0,0 +1,195 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} + _ fwxschema.AttributeWithFloat64Validators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Float64 +} + +// 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) +} + +// Float64Validators returns the Validators field value. +func (a Float64Attribute) Float64Validators() []validator.Float64 { + return a.Validators +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a Float64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a Float64Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Float64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Float64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go new file mode 100644 index 000000000..4bfb2ebd0 --- /dev/null +++ b/ephemeral/schema/float64_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: schema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: schema.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 TestFloat64AttributeFloat64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected []validator.Float64 + }{ + "no-validators": { + attribute: schema.Float64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Float64Attribute{ + Validators: []validator.Float64{}, + }, + expected: []validator.Float64{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Float64Validators() + + 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 schema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Float64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Float64Attribute{}, + other: schema.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 schema.Float64Attribute + expected string + }{ + "no-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Float64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Float64Attribute{ + MarkdownDescription: "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 schema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: schema.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 schema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Float64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Float64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Float64Attribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Float64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go new file mode 100644 index 000000000..76f6e0194 --- /dev/null +++ b/ephemeral/schema/int32_attribute.go @@ -0,0 +1,195 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int32Attribute{} + _ fwxschema.AttributeWithInt32Validators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Int32 +} + +// 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 the DeprecationMessage field value. +func (a Int32Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 +} + +// Int32Validators returns the Validators field value. +func (a Int32Attribute) Int32Validators() []validator.Int32 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int32Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int32Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int32Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go new file mode 100644 index 000000000..b4d3cc6dc --- /dev/null +++ b/ephemeral/schema/int32_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: schema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int32Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int32Attribute{}, + other: schema.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 schema.Int32Attribute + expected string + }{ + "no-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int32Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int32Attribute{ + MarkdownDescription: "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 schema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: schema.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 TestInt32AttributeInt32Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected []validator.Int32 + }{ + "no-validators": { + attribute: schema.Int32Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int32Attribute{ + Validators: []validator.Int32{}, + }, + expected: []validator.Int32{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int32Validators() + + 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 schema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int32Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int32Attribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int32Attribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int32Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go new file mode 100644 index 000000000..27cb2dd23 --- /dev/null +++ b/ephemeral/schema/int64_attribute.go @@ -0,0 +1,195 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} + _ fwxschema.AttributeWithInt64Validators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Int64 +} + +// 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 the DeprecationMessage field value. +func (a Int64Attribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 +} + +// Int64Validators returns the Validators field value. +func (a Int64Attribute) Int64Validators() []validator.Int64 { + return a.Validators +} + +// IsComputed returns the Computed field value. +func (a Int64Attribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a Int64Attribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a Int64Attribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go new file mode 100644 index 000000000..6b7c1849f --- /dev/null +++ b/ephemeral/schema/int64_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: schema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.Int64Attribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: schema.Int64Attribute{}, + other: schema.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 schema.Int64Attribute + expected string + }{ + "no-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: schema.Int64Attribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.Int64Attribute{ + MarkdownDescription: "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 schema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: schema.Int64Attribute{}, + expected: types.Int64Type, + }, + "custom-type": { + attribute: schema.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 TestInt64AttributeInt64Validators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected []validator.Int64 + }{ + "no-validators": { + attribute: schema.Int64Attribute{}, + expected: nil, + }, + "validators": { + attribute: schema.Int64Attribute{ + Validators: []validator.Int64{}, + }, + expected: []validator.Int64{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Int64Validators() + + 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 schema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "computed": { + attribute: schema.Int64Attribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "optional": { + attribute: schema.Int64Attribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "required": { + attribute: schema.Int64Attribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.Int64Attribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go new file mode 100644 index 000000000..258757f99 --- /dev/null +++ b/ephemeral/schema/list_attribute.go @@ -0,0 +1,226 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} + _ fwxschema.AttributeWithListValidators = 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. +// +// Use ListNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a list or an element directly via square brace 0-based index syntax: +// +// # first known element +// .example_attribute[0] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + 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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + Sensitive bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// 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 the DeprecationMessage field value. +func (a ListAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a ListAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ListAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// ListValidators returns the Validators field value. +func (a ListAttribute) ListValidators() []validator.List { + return a.Validators +} + +// 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 should never include false positives. +func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go new file mode 100644 index 000000000..cdbc9feb6 --- /dev/null +++ b/ephemeral/schema/list_attribute_test.go @@ -0,0 +1,525 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + other: schema.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 schema.ListAttribute + expected string + }{ + "no-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.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 schema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.ListAttribute{ + MarkdownDescription: "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 schema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.ListAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + 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 schema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.ListAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.ListAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.ListAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.ListAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.ListAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 TestListAttributeListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.ListAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + 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 schema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListAttribute{ + Computed: true, + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.ListAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.ListAttribute{ + Computed: 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.", + ), + }, + }, + }, + } + + 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) + } + }) + } +} + +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) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go new file mode 100644 index 000000000..a4478fb48 --- /dev/null +++ b/ephemeral/schema/list_nested_attribute.go @@ -0,0 +1,251 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = ListNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListNestedAttribute{} + _ fwxschema.AttributeWithListValidators = ListNestedAttribute{} +) + +// ListNestedAttribute represents an attribute that is a list of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ListAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a list of objects or directly via square and curly brace syntax. +// +// # list of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_attribute[0] +// # first known object nested_attribute value +// .example_attribute[0].nested_attribute +type ListNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyInt, otherwise returns an error. +func (a ListNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a ListNestedAttribute +// and all fields are equal. +func (a ListNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(ListNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ListNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ListNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ListNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a ListNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeList. +func (a ListNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeList +} + +// GetType returns ListType of ObjectType or CustomType. +func (a ListNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a ListNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ListNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ListNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// ListValidators returns the Validators field value. +func (a ListNestedAttribute) ListValidators() []validator.List { + return a.Validators +} + +// 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 +// should never include false positives. +func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go new file mode 100644 index 000000000..914af418c --- /dev/null +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -0,0 +1,690 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedAttribute"), + }, + } + + 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 TestListNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.ListNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestListNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 TestListNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.ListNestedAttribute{ + 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 TestListNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.ListNestedAttribute{ + MarkdownDescription: "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 TestListNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.ListNestedAttribute{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + 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 TestListNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.ListNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestListNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.ListNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestListNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.ListNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestListNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.ListNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected []validator.List + }{ + "no-validators": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.ListNestedAttribute{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.ListNestedAttribute{ + Computed: true, + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.ListNestedAttribute{ + 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{}, + }, + "nestedobject-dynamic": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + 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/ephemeral/schema/list_nested_block.go b/ephemeral/schema/list_nested_block.go new file mode 100644 index 000000000..4d098bc2d --- /dev/null +++ b/ephemeral/schema/list_nested_block.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "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/internal/fwtype" + "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. +var ( + _ Block = ListNestedBlock{} + _ fwschema.BlockWithValidateImplementation = ListNestedBlock{} + _ fwxschema.BlockWithListValidators = ListNestedBlock{} +) + +// ListNestedBlock represents a block that is a list of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.List +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer ListNestedAttribute over ListNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # list of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a list of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type ListNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.ListType of types.ObjectType. When retrieving data, the + // basetypes.ListValuable associated with this custom type must be used in + // place of types.List. + CustomType basetypes.ListTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.List +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyInt, otherwise returns an error. +func (b ListNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to ListNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is ListNestedBlock +// and all fields are equal. +func (b ListNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(ListNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b ListNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b ListNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b ListNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b ListNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeList. +func (b ListNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeList +} + +// ListValidators returns the Validators field value. +func (b ListNestedBlock) ListValidators() []validator.List { + return b.Validators +} + +// Type returns ListType of ObjectType or CustomType. +func (b ListNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.ListType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// errors or panics. This logic runs during the GetProviderSchema RPC and +// should never include false positives. +func (b ListNestedBlock) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/list_nested_block_test.go b/ephemeral/schema/list_nested_block_test.go new file mode 100644 index 000000000..763afd28c --- /dev/null +++ b/ephemeral/schema/list_nested_block_test.go @@ -0,0 +1,552 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestListNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListNestedBlock"), + }, + "ElementKeyInt": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyString": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListNestedBlock"), + }, + "ElementKeyValue": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListNestedBlock"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestListNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.ListNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithListValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.ListNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.ListNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockListValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected []validator.List + }{ + "no-validators": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.ListNestedBlock{ + Validators: []validator.List{}, + }, + expected: []validator.List{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ListValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + expected attr.Type + }{ + "base": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.ListNestedBlock{ + // CustomType: testtypes.ListType{}, + // }, + // expected: testtypes.ListType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.ListNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + 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 block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go new file mode 100644 index 000000000..0741747a4 --- /dev/null +++ b/ephemeral/schema/map_attribute.go @@ -0,0 +1,230 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = MapAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapAttribute{} + _ fwxschema.AttributeWithMapValidators = MapAttribute{} +) + +// MapAttribute represents a schema attribute that is a map with a single +// element type. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use MapNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map or directly via curly brace syntax. +// +// # map of strings +// example_attribute = { +// key1 = "first", +// key2 = "second", +// } +// +// Terraform configurations reference this attribute using expressions that +// accept a map or an element directly via square brace string syntax: +// +// # key1 known element +// .example_attribute["key1"] +type MapAttribute struct { + // ElementType is the type for all elements of the map. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.MapType. When retrieving data, the basetypes.MapValuable + // associated with this custom type must be used in place of types.Map. + CustomType basetypes.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a map +// index or an error. +func (a MapAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a MapAttribute +// and all fields are equal. +func (a MapAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(MapAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.MapType or the CustomType field value if defined. +func (a MapAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns the Computed field value. +func (a MapAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a MapAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// MapValidators returns the Validators field value. +func (a MapAttribute) MapValidators() []validator.Map { + return a.Validators +} + +// 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 should never include false positives. +func (a MapAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go new file mode 100644 index 000000000..f750b5f5d --- /dev/null +++ b/ephemeral/schema/map_attribute_test.go @@ -0,0 +1,525 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapType"), + }, + "ElementKeyInt": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapType"), + }, + "ElementKeyString": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapType"), + }, + } + + 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 TestMapAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestMapAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + other: schema.MapAttribute{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 TestMapAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.MapAttribute{ + 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 TestMapAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.MapAttribute{ + MarkdownDescription: "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 TestMapAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: types.MapType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.MapAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + 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 TestMapAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.MapAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestMapAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.MapAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestMapAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.MapAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestMapAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.MapAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.MapAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapAttribute{ + Computed: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.MapAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.MapAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.MapAttribute{ + Computed: 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.", + ), + }, + }, + }, + } + + 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/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go new file mode 100644 index 000000000..de057e106 --- /dev/null +++ b/ephemeral/schema/map_nested_attribute.go @@ -0,0 +1,251 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = MapNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = MapNestedAttribute{} + _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} +) + +// MapNestedAttribute represents an attribute that is a map of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Map +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use MapAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a map of objects or directly via curly brace syntax. +// +// # map of objects +// example_attribute = { +// key = { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a map of objects or an element directly via square brace string +// syntax: +// +// # known object at key +// .example_attribute["key"] +// # known object nested_attribute value at key +// .example_attribute["key"].nested_attribute +type MapNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.MapType of types.ObjectType. When retrieving data, the + // basetypes.MapValuable associated with this custom type must be used in + // place of types.Map. + CustomType basetypes.MapTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Map +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyString, otherwise returns an error. +func (a MapNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyString) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to MapNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a MapNestedAttribute +// and all fields are equal. +func (a MapNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(MapNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a MapNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a MapNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a MapNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeMap. +func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeMap +} + +// GetType returns MapType of ObjectType or CustomType. +func (a MapNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.MapType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a MapNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a MapNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a MapNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// MapValidators returns the Validators field value. +func (a MapNestedAttribute) MapValidators() []validator.Map { + return a.Validators +} + +// 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 +// should never include false positives. +func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go new file mode 100644 index 000000000..37c2f1aa2 --- /dev/null +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -0,0 +1,690 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to MapNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to MapNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "ElementKeyValue": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to MapNestedAttribute"), + }, + } + + 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 TestMapNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.MapNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestMapNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithMapValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 TestMapNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.MapNestedAttribute{ + 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 TestMapNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.MapNestedAttribute{ + MarkdownDescription: "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 TestMapNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.MapNestedAttribute{ + // CustomType: testtypes.MapType{}, + // }, + // expected: testtypes.MapType{}, + // }, + } + + 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 TestMapNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.MapNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestMapNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.MapNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestMapNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.MapNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestMapNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.MapNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected []validator.Map + }{ + "no-validators": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.MapNestedAttribute{ + Validators: []validator.Map{}, + }, + expected: []validator.Map{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.MapValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.MapNestedAttribute{ + Computed: true, + CustomType: testtypes.MapType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.MapNestedAttribute{ + 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{}, + }, + "nestedobject-dynamic": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + 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/ephemeral/schema/nested_attribute.go b/ephemeral/schema/nested_attribute.go new file mode 100644 index 000000000..31d2ee158 --- /dev/null +++ b/ephemeral/schema/nested_attribute.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Nested attributes are only compatible with protocol version 6. +type NestedAttribute interface { + Attribute + fwschema.NestedAttribute +} diff --git a/ephemeral/schema/nested_attribute_object.go b/ephemeral/schema/nested_attribute_object.go new file mode 100644 index 000000000..3719a2398 --- /dev/null +++ b/ephemeral/schema/nested_attribute_object.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedAttributeObjectWithValidators = NestedAttributeObject{} + +// NestedAttributeObject is the object containing the underlying attributes +// for a ListNestedAttribute, MapNestedAttribute, SetNestedAttribute, or +// SingleNestedAttribute (automatically generated). When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. The Attributes field must be set. Nested attributes are only +// compatible with protocol version 6. +// +// This object enables customizing and simplifying details within its parent +// NestedAttribute, therefore it cannot have Terraform schema fields such as +// Required, Description, etc. +type NestedAttributeObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedAttributeObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedAttributeObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedAttributeObject is equivalent. +func (o NestedAttributeObject) Equal(other fwschema.NestedAttributeObject) bool { + if _, ok := other.(NestedAttributeObject); !ok { + return false + } + + return fwschema.NestedAttributeObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedAttributeObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// ObjectValidators returns the Validators field value. +func (o NestedAttributeObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedAttributeObject. +func (o NestedAttributeObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedAttributeObjectType(o) +} diff --git a/ephemeral/schema/nested_attribute_object_test.go b/ephemeral/schema/nested_attribute_object_test.go new file mode 100644 index 000000000..f4827f212 --- /dev/null +++ b/ephemeral/schema/nested_attribute_object_test.go @@ -0,0 +1,270 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "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 TestNestedAttributeObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on NestedAttributeObject"), + }, + "ElementKeyInt": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedAttributeObject"), + }, + "ElementKeyString": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedAttributeObject"), + }, + "ElementKeyValue": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedAttributeObject"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.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 TestNestedAttributeObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + other fwschema.NestedAttributeObject + expected bool + }{ + "different-attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedAttributeObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedAttributeObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedAttributeObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedAttributeObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedAttributeObject + expected attr.Type + }{ + "base": { + object: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/nested_block_object.go b/ephemeral/schema/nested_block_object.go new file mode 100644 index 000000000..2b560b606 --- /dev/null +++ b/ephemeral/schema/nested_block_object.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ fwxschema.NestedBlockObjectWithValidators = NestedBlockObject{} + +// NestedBlockObject is the object containing the underlying attributes and +// blocks for a ListNestedBlock or SetNestedBlock. When retrieving the value +// for this attribute, use types.Object as the value type unless the CustomType +// field is set. +// +// This object enables customizing and simplifying details within its parent +// Block, therefore it cannot have Terraform schema fields such as Description, +// etc. +type NestedBlockObject struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep performs an AttributeName step on the +// underlying attributes or returns an error. +func (o NestedBlockObject) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.NestedBlockObjectApplyTerraform5AttributePathStep(o, step) +} + +// Equal returns true if the given NestedBlockObject is equivalent. +func (o NestedBlockObject) Equal(other fwschema.NestedBlockObject) bool { + if _, ok := other.(NestedBlockObject); !ok { + return false + } + + return fwschema.NestedBlockObjectEqual(o, other) +} + +// GetAttributes returns the Attributes field value. +func (o NestedBlockObject) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(o.Attributes) +} + +// GetAttributes returns the Blocks field value. +func (o NestedBlockObject) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(o.Blocks) +} + +// ObjectValidators returns the Validators field value. +func (o NestedBlockObject) ObjectValidators() []validator.Object { + return o.Validators +} + +// Type returns the framework type of the NestedBlockObject. +func (o NestedBlockObject) Type() basetypes.ObjectTypable { + if o.CustomType != nil { + return o.CustomType + } + + return fwschema.NestedBlockObjectType(o) +} diff --git a/ephemeral/schema/nested_block_object_test.go b/ephemeral/schema/nested_block_object_test.go new file mode 100644 index 000000000..7c2ebffc5 --- /dev/null +++ b/ephemeral/schema/nested_block_object_test.go @@ -0,0 +1,354 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "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 TestNestedBlockObjectApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on NestedBlockObject"), + }, + "ElementKeyInt": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to NestedBlockObject"), + }, + "ElementKeyString": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to NestedBlockObject"), + }, + "ElementKeyValue": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to NestedBlockObject"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.object.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 TestNestedBlockObjectEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + other fwschema.NestedBlockObject + expected bool + }{ + "different-attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected fwschema.UnderlyingAttributes + }{ + "no-attributes": { + object: schema.NestedBlockObject{}, + expected: fwschema.UnderlyingAttributes{}, + }, + "attributes": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: fwschema.UnderlyingAttributes{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected map[string]fwschema.Block + }{ + "no-blocks": { + object: schema.NestedBlockObject{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + object: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NestedBlockObject + expected []validator.Object + }{ + "no-validators": { + attribute: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.NestedBlockObject{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNestedBlockObjectType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + object schema.NestedBlockObject + expected attr.Type + }{ + "base": { + object: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.object.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go new file mode 100644 index 000000000..17d557398 --- /dev/null +++ b/ephemeral/schema/number_attribute.go @@ -0,0 +1,196 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} + _ fwxschema.AttributeWithNumberValidators = 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 +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Number +} + +// 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 the DeprecationMessage field value. +func (a NumberAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a NumberAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a NumberAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a NumberAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// 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 new file mode 100644 index 000000000..a15142afa --- /dev/null +++ b/ephemeral/schema/number_attribute_test.go @@ -0,0 +1,430 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: schema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.NumberAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: schema.NumberAttribute{}, + other: schema.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 schema.NumberAttribute + expected string + }{ + "no-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.NumberAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.NumberAttribute{ + MarkdownDescription: "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 schema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: schema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: schema.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 schema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.NumberAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.NumberAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "required": { + attribute: schema.NumberAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.NumberAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected []validator.Number + }{ + "no-validators": { + attribute: schema.NumberAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.NumberAttribute{ + Validators: []validator.Number{}, + }, + expected: []validator.Number{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.NumberValidators() + + 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 new file mode 100644 index 000000000..fa808e4ba --- /dev/null +++ b/ephemeral/schema/object_attribute.go @@ -0,0 +1,228 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = ObjectAttribute{} + _ fwschema.AttributeWithValidateImplementation = ObjectAttribute{} + _ fwxschema.AttributeWithObjectValidators = ObjectAttribute{} +) + +// ObjectAttribute represents a schema attribute that is an object with only +// type information for underlying attributes. When retrieving the value for +// this attribute, use types.Object as the value type unless the CustomType +// field is set. The AttributeTypes field must be set. +// +// Prefer SingleNestedAttribute over ObjectAttribute if the provider is +// using protocol version 6 and full attribute functionality is needed. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # object with one attribute +// example_attribute = { +// underlying_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute directly via period syntax: +// +// # underlying attribute +// .example_attribute.underlying_attribute +type ObjectAttribute struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + // + // Attribute types that contain a collection with a nested dynamic type (i.e. types.List[types.Dynamic]) are not supported. + // If underlying dynamic collection values are required, replace this attribute definition with + // DynamicAttribute instead. + AttributeTypes map[string]attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // Sensitive indicates whether the value of this attribute should be + // considered sensitive data. Setting it to true will obscure the value + Sensitive bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into an +// attribute name or an error. +func (a ObjectAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ObjectAttribute +// and all fields are equal. +func (a ObjectAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ObjectAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a ObjectAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a ObjectAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a ObjectAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.ObjectType or the CustomType field value if defined. +func (a ObjectAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ObjectType{ + AttrTypes: a.AttributeTypes, + } +} + +// IsComputed returns the Computed field value. +func (a ObjectAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a ObjectAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a ObjectAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// ObjectValidators returns the Validators field value. +func (a ObjectAttribute) ObjectValidators() []validator.Object { + return a.Validators +} + +// 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 should never include false positives. +func (a ObjectAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.AttributeTypes == nil && a.CustomType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingAttributeTypesDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go new file mode 100644 index 000000000..f49e4afca --- /dev/null +++ b/ephemeral/schema/object_attribute_test.go @@ -0,0 +1,559 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("testattr"), + expected: types.StringType, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("undefined attribute name other in ObjectType"), + }, + "ElementKeyInt": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to ObjectType"), + }, + "ElementKeyString": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ObjectType"), + }, + "ElementKeyValue": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ObjectType"), + }, + } + + 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 TestObjectAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "deprecation-message": { + attribute: schema.ObjectAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestObjectAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attribute-type": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.BoolType}}, + expected: false, + }, + "equal": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + other: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": 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 TestObjectAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "description": { + attribute: schema.ObjectAttribute{ + 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 TestObjectAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: "", + }, + "markdown-description": { + attribute: schema.ObjectAttribute{ + MarkdownDescription: "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 TestObjectAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected attr.Type + }{ + "base": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, + }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + 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 TestObjectAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-computed": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "computed": { + attribute: schema.ObjectAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestObjectAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optional": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "optional": { + attribute: schema.ObjectAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestObjectAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-required": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "required": { + attribute: schema.ObjectAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestObjectAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: false, + }, + "sensitive": { + attribute: schema.ObjectAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 TestObjectAttributeObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, + expected: nil, + }, + "validators": { + attribute: schema.ObjectAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +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() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "attributetypes": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + "test_list": types.ListType{ + ElemType: types.StringType, + }, + "test_obj": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attr": types.DynamicType, + }, + }, + }, + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "attributetypes-nested-collection-dynamic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.ListType{ + ElemType: types.DynamicType, + }, + }, + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "attributetypes-missing": { + attribute: schema.ObjectAttribute{ + Computed: 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 AttributeTypes or CustomType field on an object Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + "customtype": { + attribute: schema.ObjectAttribute{ + Computed: true, + CustomType: testtypes.ObjectType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + } + + 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/ephemeral/schema/schema.go b/ephemeral/schema/schema.go new file mode 100644 index 000000000..3c92269fb --- /dev/null +++ b/ephemeral/schema/schema.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +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 ephemeral resource data. This type +// is used as the ephemeral.SchemaResponse type Schema field, which is +// implemented by the ephemeral.EphemeralResource type Schema method. +type Schema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this ephemeral resource is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this ephemeral resource is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this ephemeral resource. The warning diagnostic + // summary is automatically set to "Ephemeral Resource Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Use examplecloud_other ephemeral resource instead. This ephemeral resource + // will be removed in the next major version of the provider." + // - "Remove this ephemeral resource as it no longer is valid and + // will be removed in the next major version of the provider." + // + DeprecationMessage string +} + +// 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 the Blocks field value. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(s.Blocks) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (s Schema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (s Schema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (s Schema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion always returns 0 as ephemeral resource schemas cannot be versioned. +func (s Schema) GetVersion() int64 { + return 0 +} + +// 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 GetProviderSchema +// 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)...) + } + + for blockName, block := range s.GetBlocks() { + req := fwschema.ValidateImplementationRequest{ + Name: blockName, + Path: path.Root(blockName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateBlockImplementation(ctx, block, req)...) + } + + return diags +} + +// schemaAttributes is a ephemeral resource 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 +} + +// schemaBlocks is a ephemeral resource to fwschema type conversion function. +func schemaBlocks(blocks map[string]Block) map[string]fwschema.Block { + result := make(map[string]fwschema.Block, len(blocks)) + + for name, block := range blocks { + result[name] = block + } + + return result +} diff --git a/ephemeral/schema/schema_test.go b/ephemeral/schema/schema_test.go new file mode 100644 index 000000000..e08d76a5d --- /dev/null +++ b/ephemeral/schema/schema_test.go @@ -0,0 +1,1331 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 schema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: schema.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 schema.Schema", + ), + }, + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("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: "+fwschema.ErrPathIsBlock.Error(), + ), + }, + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "empty-nil": { + schema: schema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "nil": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "other": schema.BoolAttribute{}, + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: schema.StringAttribute{}, + }, + "WithAttributeName-block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "other": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "otherattr": schema.StringAttribute{}, + }, + }, + "test": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: nil, + expectedErr: fwschema.ErrPathIsBlock.Error(), + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.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 schema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: schema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.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 schema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: schema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + "blocks": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: map[string]fwschema.Block{ + "testblock1": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + "testblock2": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + } + + 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 schema.Schema + expected string + }{ + "no-deprecation-message": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + schema: schema.Schema{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.Schema + expected string + }{ + "no-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + schema: schema.Schema{ + Description: "test description", + }, + expected: "test description", + }, + } + + 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 schema.Schema + expected string + }{ + "no-markdown-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + schema: schema.Schema{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + 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 schema.Schema + expected int64 + }{ + "0": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + } + + 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 schema.Schema + expected attr.Type + }{ + "base": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": 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 schema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: path.Root("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.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: schema.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: schema.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: schema.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 schema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: schema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool": schema.BoolAttribute{}, + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-Block": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "list_block_nested": schema.StringAttribute{}, + }, + }, + }, + "set_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "set_block_nested": schema.StringAttribute{}, + }, + }, + }, + "single_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "single_block_nested": schema.StringAttribute{}, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("list_block"), + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list_block_nested": types.StringType, + }, + }, + }, + }, + "AttributeName-non-existent": { + schema: schema.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: schema.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: schema.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: schema.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 schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "validate-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.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 schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.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.", + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + 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"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + 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.", + ), + 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"+ + "\"connection\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.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 (_).", + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "^": schema.ListNestedBlock{}, + }, + }, + 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 (_).", + ), + }, + }, + "nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + 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 \"single_nested_attribute.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + 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 \"single_nested_block.^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + 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 (_).", + ), + 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 (_).", + ), + 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: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: 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.", + ), + }, + }, + "nested-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: 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"+ + "\"list_nested_attribute.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.", + ), + }, + }, + "nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: 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"+ + "\"list_nested_block.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.", + ), + }, + }, + "nested-nested-block-attribute-with-validate-attribute-implementation-error": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "list_nested_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test": schema.ListAttribute{ + Computed: 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"+ + "\"list_nested_block.list_nested_nested_block.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/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go new file mode 100644 index 000000000..7ecd08ffd --- /dev/null +++ b/ephemeral/schema/set_attribute.go @@ -0,0 +1,225 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = SetAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetAttribute{} + _ fwxschema.AttributeWithSetValidators = SetAttribute{} +) + +// SetAttribute represents a schema attribute that is a set with a single +// element type. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// Use SetNestedAttribute if the underlying elements should be objects and +// require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set or directly via square brace syntax. +// +// # set of strings +// example_attribute = ["first", "second"] +// +// Terraform configurations reference this attribute using expressions that +// accept a set. Sets cannot be indexed in Terraform, therefore an expression +// is required to access an explicit element. +type SetAttribute struct { + // ElementType is the type for all elements of the set. This field must be + // set. + // + // Element types that contain a dynamic type (i.e. types.Dynamic) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.SetType. When retrieving data, the basetypes.SetValuable + // associated with this custom type must be used in place of types.Set. + CustomType basetypes.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a set +// index or an error. +func (a SetAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a SetAttribute +// and all fields are equal. +func (a SetAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(SetAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.SetType or the CustomType field value if defined. +func (a SetAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns the Computed field value. +func (a SetAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SetAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// SetValidators returns the Validators field value. +func (a SetAttribute) SetValidators() []validator.Set { + return a.Validators +} + +// 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 should never include false positives. +func (a SetAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go new file mode 100644 index 000000000..d3c158fda --- /dev/null +++ b/ephemeral/schema/set_attribute_test.go @@ -0,0 +1,525 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetType"), + }, + "ElementKeyInt": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetType"), + }, + "ElementKeyString": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetType"), + }, + "ElementKeyValue": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: types.StringType, + expectedError: nil, + }, + } + + 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 TestSetAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestSetAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-element-type": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + other: schema.SetAttribute{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 TestSetAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: schema.SetAttribute{ + 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 TestSetAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description": { + attribute: schema.SetAttribute{ + MarkdownDescription: "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 TestSetAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: types.SetType{ElemType: types.StringType}, + }, + // "custom-type": { + // attribute: schema.SetAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + 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 TestSetAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "computed": { + attribute: schema.SetAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestSetAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "optional": { + attribute: schema.SetAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestSetAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "required": { + attribute: schema.SetAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestSetAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: false, + }, + "sensitive": { + attribute: schema.SetAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetAttribute{ElementType: types.StringType}, + expected: nil, + }, + "validators": { + attribute: schema.SetAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{}, + Optional: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype": { + attribute: schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "elementtype-dynamic": { + attribute: schema.SetAttribute{ + Computed: true, + ElementType: types.DynamicType, + }, + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + "elementtype-missing": { + attribute: schema.SetAttribute{ + Computed: 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.", + ), + }, + }, + }, + } + + 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/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go new file mode 100644 index 000000000..658dc0df7 --- /dev/null +++ b/ephemeral/schema/set_nested_attribute.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +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" + "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SetNestedAttribute{} + _ fwschema.AttributeWithValidateImplementation = SetNestedAttribute{} + _ fwxschema.AttributeWithSetValidators = SetNestedAttribute{} +) + +// SetNestedAttribute represents an attribute that is a set of objects where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use SetAttribute if the underlying elements are of a single type and do +// not require definition beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return a set of objects or directly via square and curly brace syntax. +// +// # set of objects +// example_attribute = [ +// { +// nested_attribute = #... +// }, +// ] +// +// Terraform configurations reference this attribute using expressions that +// accept a set of objects. Sets cannot be indexed in Terraform, therefore +// an expression is required to access an explicit element. +type SetNestedAttribute struct { + // NestedObject is the underlying object that contains nested attributes. + // This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this attribute definition with + // DynamicAttribute instead. + NestedObject NestedAttributeObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is ElementKeyValue, otherwise returns an error. +func (a SetNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedAttribute", step) + } + + return a.NestedObject, nil +} + +// Equal returns true if the given Attribute is a SetNestedAttribute +// and all fields are equal. +func (a SetNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SetNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SetNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SetNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SetNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return a.NestedObject +} + +// GetNestingMode always returns NestingModeSet. +func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSet +} + +// GetType returns SetType of ObjectType or CustomType. +func (a SetNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.SetType{ + ElemType: a.NestedObject.Type(), + } +} + +// IsComputed returns the Computed field value. +func (a SetNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SetNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SetNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// SetValidators returns the Validators field value. +func (a SetNestedAttribute) SetValidators() []validator.Set { + return a.Validators +} + +// 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 +// should never include false positives. +func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go new file mode 100644 index 000000000..3abd59839 --- /dev/null +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -0,0 +1,691 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_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/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + 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 TestSetNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SetNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestSetNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.AttributeWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 TestSetNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SetNestedAttribute{ + 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 TestSetNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SetNestedAttribute{ + MarkdownDescription: "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 TestSetNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + // "custom-type": { + // attribute: schema.SetNestedAttribute{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + 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 TestSetNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SetNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestSetNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SetNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestSetNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SetNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestSetNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SetNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected []validator.Set + }{ + "no-validators": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SetNestedAttribute{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + attribute: schema.SetNestedAttribute{ + Computed: true, + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + attribute: schema.SetNestedAttribute{ + 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{}, + }, + "nestedobject-dynamic": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + 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 an attribute that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + ), + }, + }, + }, + } + + 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/ephemeral/schema/set_nested_block.go b/ephemeral/schema/set_nested_block.go new file mode 100644 index 000000000..085163f37 --- /dev/null +++ b/ephemeral/schema/set_nested_block.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + "fmt" + + "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/internal/fwtype" + "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. +var ( + _ Block = SetNestedBlock{} + _ fwschema.BlockWithValidateImplementation = SetNestedBlock{} + _ fwxschema.BlockWithSetValidators = SetNestedBlock{} +) + +// SetNestedBlock represents a block that is a set of objects where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Set +// as the value type unless the CustomType field is set. The NestedObject field +// must be set. +// +// Prefer SetNestedAttribute over SetNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block repeatedly using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # set of blocks with two elements +// example_block { +// nested_attribute = #... +// } +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept a set of objects or an element directly via square brace 0-based +// index syntax: +// +// # first known object +// .example_block[0] +// # first known object nested_attribute value +// .example_block[0].nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SetNestedBlock struct { + // NestedObject is the underlying object that contains nested attributes or + // blocks. This field must be set. + // + // Nested attributes that contain a dynamic type (i.e. DynamicAttribute) are not supported. + // If underlying dynamic values are required, replace this block definition with + // a DynamicAttribute. + NestedObject NestedBlockObject + + // CustomType enables the use of a custom attribute type in place of the + // default types.SetType of types.ObjectType. When retrieving data, the + // basetypes.SetValuable associated with this custom type must be used in + // place of types.Set. + CustomType basetypes.SetTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Set +} + +// ApplyTerraform5AttributePathStep returns the NestedObject field value if step +// is ElementKeyValue, otherwise returns an error. +func (b SetNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SetNestedBlock", step) + } + + return b.NestedObject, nil +} + +// Equal returns true if the given Block is SetNestedBlock +// and all fields are equal. +func (b SetNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SetNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SetNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SetNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SetNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns the NestedObject field value. +func (b SetNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return b.NestedObject +} + +// GetNestingMode always returns BlockNestingModeSet. +func (b SetNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSet +} + +// SetValidators returns the Validators field value. +func (b SetNestedBlock) SetValidators() []validator.Set { + return b.Validators +} + +// Type returns SetType of ObjectType or CustomType. +func (b SetNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + return types.SetType{ + ElemType: b.NestedObject.Type(), + } +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the block to prevent unexpected +// 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 b.CustomType == nil && fwtype.ContainsCollectionWithDynamic(b.Type()) { + resp.Diagnostics.Append(fwtype.BlockCollectionWithDynamicTypeDiag(req.Path)) + } +} diff --git a/ephemeral/schema/set_nested_block_test.go b/ephemeral/schema/set_nested_block_test.go new file mode 100644 index 000000000..d403d4486 --- /dev/null +++ b/ephemeral/schema/set_nested_block_test.go @@ -0,0 +1,552 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSetNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to SetNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SetNestedBlock"), + }, + "ElementKeyString": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SetNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestSetNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SetNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: testschema.BlockWithSetValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + other: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "description": { + block: schema.SetNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SetNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockSetValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected []validator.Set + }{ + "no-validators": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SetNestedBlock{ + Validators: []validator.Set{}, + }, + expected: []validator.Set{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.SetValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SetNestedBlock{ + // CustomType: testtypes.SetType{}, + // }, + // expected: testtypes.SetType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedBlockValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SetNestedBlock + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "customtype": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{}, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "nestedobject-dynamic": { + block: schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "test_dyn": schema.DynamicAttribute{ + 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 block that contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the \"test\" block definition with a DynamicAttribute.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.block.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go new file mode 100644 index 000000000..167b8c136 --- /dev/null +++ b/ephemeral/schema/single_nested_attribute.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +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/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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ NestedAttribute = SingleNestedAttribute{} + _ fwxschema.AttributeWithObjectValidators = SingleNestedAttribute{} +) + +// SingleNestedAttribute represents an attribute that is a single object where +// the object attributes can be fully defined, including further nested +// attributes. When retrieving the value for this attribute, use types.Object +// as the value type unless the CustomType field is set. The Attributes field +// must be set. Nested attributes are only compatible with protocol version 6. +// +// Use ObjectAttribute if the underlying attributes do not require definition +// beyond type information. +// +// Terraform configurations configure this attribute using expressions that +// return an object or directly via curly brace syntax. +// +// # single object +// example_attribute = { +// nested_attribute = #... +// } +// +// Terraform configurations reference this attribute using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_attribute.nested_attribute +type SingleNestedAttribute struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. This field must be set. + Attributes map[string]Attribute + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (a SingleNestedAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedAttribute", step) + } + + attribute, ok := a.Attributes[string(name)] + + if !ok { + return nil, fmt.Errorf("no attribute %q on SingleNestedAttribute", name) + } + + return attribute, nil +} + +// Equal returns true if the given Attribute is a SingleNestedAttribute +// and all fields are equal. +func (a SingleNestedAttribute) Equal(o fwschema.Attribute) bool { + other, ok := o.(SingleNestedAttribute) + + if !ok { + return false + } + + return fwschema.NestedAttributesEqual(a, other) +} + +// GetAttributes returns the Attributes field value. +func (a SingleNestedAttribute) GetAttributes() fwschema.UnderlyingAttributes { + return schemaAttributes(a.Attributes) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a SingleNestedAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a SingleNestedAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a SingleNestedAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetNestedObject returns a generated NestedAttributeObject from the +// Attributes, CustomType, and Validators field values. +func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { + return NestedAttributeObject{ + Attributes: a.Attributes, + CustomType: a.CustomType, + Validators: a.Validators, + } +} + +// GetNestingMode always returns NestingModeSingle. +func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { + return fwschema.NestingModeSingle +} + +// GetType returns ListType of ObjectType or CustomType. +func (a SingleNestedAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + attrTypes := make(map[string]attr.Type, len(a.Attributes)) + + for name, attribute := range a.Attributes { + attrTypes[name] = attribute.GetType() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} + +// IsComputed returns the Computed field value. +func (a SingleNestedAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a SingleNestedAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a SingleNestedAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// 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 new file mode 100644 index 000000000..8f4e4e783 --- /dev/null +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -0,0 +1,571 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute \"other\" on SingleNestedAttribute"), + }, + "ElementKeyInt": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedAttribute"), + }, + "ElementKeyString": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedAttribute"), + }, + "ElementKeyValue": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedAttribute"), + }, + } + + 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 TestSingleNestedAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.AttributeWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "equal": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.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 TestSingleNestedAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + attribute: schema.SingleNestedAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 TestSingleNestedAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + attribute: schema.SingleNestedAttribute{ + 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 TestSingleNestedAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + attribute: schema.SingleNestedAttribute{ + MarkdownDescription: "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 TestSingleNestedAttributeGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected schema.NestedAttributeObject + }{ + "nested-object": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected attr.Type + }{ + "base": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, + } + + 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 TestSingleNestedAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-computed": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "computed": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 TestSingleNestedAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optional": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "optional": { + attribute: schema.SingleNestedAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 TestSingleNestedAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-required": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "required": { + attribute: schema.SingleNestedAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 TestSingleNestedAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: false, + }, + "sensitive": { + attribute: schema.SingleNestedAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected []validator.Object + }{ + "no-validators": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + attribute: schema.SingleNestedAttribute{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_block.go b/ephemeral/schema/single_nested_block.go new file mode 100644 index 000000000..926825a03 --- /dev/null +++ b/ephemeral/schema/single_nested_block.go @@ -0,0 +1,213 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "fmt" + + "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. +var ( + _ Block = SingleNestedBlock{} + _ fwxschema.BlockWithObjectValidators = SingleNestedBlock{} +) + +// SingleNestedBlock represents a block that is a single object where +// the object attributes can be fully defined, including further attributes +// or blocks. When retrieving the value for this block, use types.Object +// as the value type unless the CustomType field is set. +// +// Prefer SingleNestedAttribute over SingleNestedBlock if the provider is +// using protocol version 6. Nested attributes allow practitioners to configure +// values directly with expressions. +// +// Terraform configurations configure this block only once using curly brace +// syntax without an equals (=) sign or [Dynamic Block Expressions]. +// +// # single block +// example_block { +// nested_attribute = #... +// } +// +// Terraform configurations reference this block using expressions that +// accept an object or an attribute name directly via period syntax: +// +// # object nested_attribute value +// .example_block.nested_attribute +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +type SingleNestedBlock struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ObjectType. When retrieving data, the basetypes.ObjectValuable + // associated with this custom type must be used in place of types.Object. + CustomType basetypes.ObjectTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.Object +} + +// ApplyTerraform5AttributePathStep returns the Attributes field value if step +// is AttributeName, otherwise returns an error. +func (b SingleNestedBlock) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + name, ok := step.(tftypes.AttributeName) + + if !ok { + return nil, fmt.Errorf("cannot apply step %T to SingleNestedBlock", step) + } + + if attribute, ok := b.Attributes[string(name)]; ok { + return attribute, nil + } + + if block, ok := b.Blocks[string(name)]; ok { + return block, nil + } + + return nil, fmt.Errorf("no attribute or block %q on SingleNestedBlock", name) +} + +// Equal returns true if the given Attribute is b SingleNestedBlock +// and all fields are equal. +func (b SingleNestedBlock) Equal(o fwschema.Block) bool { + if _, ok := o.(SingleNestedBlock); !ok { + return false + } + + return fwschema.BlocksEqual(b, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (b SingleNestedBlock) GetDeprecationMessage() string { + return b.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (b SingleNestedBlock) GetDescription() string { + return b.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (b SingleNestedBlock) GetMarkdownDescription() string { + return b.MarkdownDescription +} + +// GetNestedObject returns a generated NestedBlockObject from the +// Attributes, CustomType, and Validators field values. +func (b SingleNestedBlock) GetNestedObject() fwschema.NestedBlockObject { + return NestedBlockObject{ + Attributes: b.Attributes, + Blocks: b.Blocks, + CustomType: b.CustomType, + Validators: b.Validators, + } +} + +// GetNestingMode always returns BlockNestingModeSingle. +func (b SingleNestedBlock) GetNestingMode() fwschema.BlockNestingMode { + return fwschema.BlockNestingModeSingle +} + +// ObjectValidators returns the Validators field value. +func (b SingleNestedBlock) ObjectValidators() []validator.Object { + return b.Validators +} + +// Type returns ObjectType or CustomType. +func (b SingleNestedBlock) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + + attrTypes := make(map[string]attr.Type, len(b.Attributes)+len(b.Blocks)) + + for name, attribute := range b.Attributes { + attrTypes[name] = attribute.GetType() + } + + for name, block := range b.Blocks { + attrTypes[name] = block.Type() + } + + return types.ObjectType{ + AttrTypes: attrTypes, + } +} diff --git a/ephemeral/schema/single_nested_block_test.go b/ephemeral/schema/single_nested_block_test.go new file mode 100644 index 000000000..6c6a74639 --- /dev/null +++ b/ephemeral/schema/single_nested_block_test.go @@ -0,0 +1,469 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSingleNestedBlockApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-block": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + step: tftypes.AttributeName("testblock"), + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expectedError: nil, + }, + "AttributeName-missing": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("no attribute or block \"other\" on SingleNestedBlock"), + }, + "ElementKeyInt": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyInt to SingleNestedBlock"), + }, + "ElementKeyString": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to SingleNestedBlock"), + }, + "ElementKeyValue": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to SingleNestedBlock"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.block.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 TestSingleNestedBlockGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-deprecation-message": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + block: schema.SingleNestedBlock{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + other fwschema.Block + expected bool + }{ + "different-type": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: testschema.BlockWithObjectValidators{}, + expected: false, + }, + "different-attributes-definitions": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + expected: false, + }, + "different-attributes-types": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.BoolAttribute{}, + }, + }, + expected: false, + }, + "different-blocks-definitions": { + block: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + }, + other: schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: false, + }, + "equal": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + other: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + block: schema.SingleNestedBlock{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected string + }{ + "no-markdown-description": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + block: schema.SingleNestedBlock{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockGetNestedObject(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected schema.NestedBlockObject + }{ + "nested-object": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.GetNestedObject() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockObjectValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected []validator.Object + }{ + "no-validators": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: nil, + }, + "validators": { + block: schema.SingleNestedBlock{ + Validators: []validator.Object{}, + }, + expected: []validator.Object{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.ObjectValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block schema.SingleNestedBlock + expected attr.Type + }{ + "base": { + block: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "testblock": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + "testblock": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + }, + }, + // "custom-type": { + // block: schema.SingleNestedBlock{ + // CustomType: testtypes.SingleType{}, + // }, + // expected: testtypes.SingleType{}, + // }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go new file mode 100644 index 000000000..3cfefe97c --- /dev/null +++ b/ephemeral/schema/string_attribute.go @@ -0,0 +1,192 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// Ensure the implementation satisifies the desired interfaces. +var ( + _ Attribute = StringAttribute{} + _ fwxschema.AttributeWithStringValidators = 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" +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +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 + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true, + // and Required and Computed cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Computed indicates whether the provider may return its own value for + // this Attribute or not. Required and Computed cannot both be true. If + // Required and Optional are both false, Computed must be true, and the + // attribute will be considered "read only" for the practitioner, with + // only the provider able to set its value. + Computed bool + + // 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 + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.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 the DeprecationMessage field value. +func (a StringAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a StringAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// 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 the Computed field value. +func (a StringAttribute) IsComputed() bool { + return a.Computed +} + +// IsOptional returns the Optional field value. +func (a StringAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a StringAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns the Sensitive field value. +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 +} + +// 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 new file mode 100644 index 000000000..9f8532f34 --- /dev/null +++ b/ephemeral/schema/string_attribute_test.go @@ -0,0 +1,429 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "fmt" + "strings" + "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" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "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) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: schema.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 schema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.StringAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + 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 schema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: schema.StringAttribute{}, + other: schema.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 schema.StringAttribute + expected string + }{ + "no-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: schema.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 schema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.StringAttribute{ + MarkdownDescription: "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 schema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: schema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: schema.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 schema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.StringAttribute{ + Computed: true, + }, + expected: true, + }, + } + + 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 schema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.StringAttribute{ + Optional: true, + }, + expected: true, + }, + } + + 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 schema.StringAttribute + expected bool + }{ + "not-required": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: schema.StringAttribute{ + Required: true, + }, + expected: true, + }, + } + + 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 schema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.StringAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + 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 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() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []validator.String + }{ + "no-validators": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.StringAttribute{ + Validators: []validator.String{}, + }, + expected: []validator.String{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/validate_config.go b/ephemeral/validate_config.go new file mode 100644 index 000000000..fc3328b67 --- /dev/null +++ b/ephemeral/validate_config.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeral + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateConfigRequest represents a request to validate the +// configuration of an ephemeral resource. An instance of this request struct is +// supplied as an argument to the EphemeralResource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigRequest struct { + // Config is the configuration the user supplied for the ephemeral resource. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config tfsdk.Config +} + +// ValidateConfigResponse represents a response to a +// ValidateConfigRequest. An instance of this response struct is +// supplied as an argument to the EphemeralResource ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigResponse struct { + // Diagnostics report errors or warnings related to validating the ephemeral resource + // configuration. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics diag.Diagnostics +} 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 bbe8dfada..207c0347e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/hashicorp/terraform-plugin-framework -go 1.21 +go 1.22.0 -toolchain go1.21.6 +toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.23.0 + github.com/hashicorp/terraform-plugin-go v0.26.0 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -14,21 +14,21 @@ 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.0 // indirect + github.com/hashicorp/go-plugin v1.6.2 // 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 - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect 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.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/grpc v1.69.4 // indirect + google.golang.org/protobuf v1.36.3 // indirect ) diff --git a/go.sum b/go.sum index 352a8f86a..5a586522d 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/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.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= -github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +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-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.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= -github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= +github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= github.com/hashicorp/terraform-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= @@ -31,8 +37,9 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= @@ -40,29 +47,41 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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_test.go b/internal/fromproto5/applyresourcechange_test.go index 6aca21466..859181364 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -250,8 +250,6 @@ func TestApplyResourceChangeRequest(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/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 3a6347dc4..737354888 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -75,3 +76,29 @@ func ImportStateClientCapabilities(in *tfprotov5.ImportResourceStateClientCapabi DeferralAllowed: in.DeferralAllowed, } } + +func OpenEphemeralResourceClientCapabilities(in *tfprotov5.OpenEphemeralResourceClientCapabilities) ephemeral.OpenClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return ephemeral.OpenClientCapabilities{ + DeferralAllowed: false, + } + } + + return ephemeral.OpenClientCapabilities{ + 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.go b/internal/fromproto5/closeephemeralresource.go new file mode 100644 index 000000000..5aa352795 --- /dev/null +++ b/internal/fromproto5/closeephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// CloseEphemeralResourceRequest returns the *fwserver.CloseEphemeralResourceRequest +// equivalent of a *tfprotov5.CloseEphemeralResourceRequest. +func CloseEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.CloseEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.CloseEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.CloseEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto5/closeephemeralresource_test.go b/internal/fromproto5/closeephemeralresource_test.go new file mode 100644 index 000000000..777d2c14b --- /dev/null +++ b/internal/fromproto5/closeephemeralresource_test.go @@ -0,0 +1,99 @@ +// 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-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov5.CloseEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.CloseEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.CloseEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov5.CloseEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.CloseEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); 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/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.go b/internal/fromproto5/ephemeral_result_data.go new file mode 100644 index 000000000..b33e88965 --- /dev/null +++ b/internal/fromproto5/ephemeral_result_data.go @@ -0,0 +1,53 @@ +// 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" +) + +// EphemeralResultData returns the *tfsdk.EphemeralResultData for a *tfprotov5.DynamicValue and +// fwschema.Schema. +func EphemeralResultData(ctx context.Context, proto5DynamicValue *tfprotov5.DynamicValue, schema fwschema.Schema) (*tfsdk.EphemeralResultData, 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 Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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"+ + "Missing schema.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto5DynamicValue, schema, fwschemadata.DataDescriptionEphemeralResultData) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.EphemeralResultData{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto5/ephemeral_result_data_test.go b/internal/fromproto5/ephemeral_result_data_test.go new file mode 100644 index 000000000..0af6479bd --- /dev/null +++ b/internal/fromproto5/ephemeral_result_data_test.go @@ -0,0 +1,120 @@ +// 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 TestEphemeralResultData(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{ + Required: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.DynamicValue + schema fwschema.Schema + expected *tfsdk.EphemeralResultData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "missing-schema": { + input: &testProto5DynamicValue, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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"+ + "Missing schema.", + ), + }, + }, + "invalid-schema": { + input: &testProto5DynamicValue, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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: &testProto5DynamicValue, + schema: testFwSchema, + expected: &tfsdk.EphemeralResultData{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.EphemeralResultData(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/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/importresourcestate_test.go b/internal/fromproto5/importresourcestate_test.go index ba58522af..39b5fdfa4 100644 --- a/internal/fromproto5/importresourcestate_test.go +++ b/internal/fromproto5/importresourcestate_test.go @@ -119,8 +119,6 @@ func TestImportResourceStateRequest(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/moveresourcestate_test.go b/internal/fromproto5/moveresourcestate_test.go index 3c8dd5181..2122edb75 100644 --- a/internal/fromproto5/moveresourcestate_test.go +++ b/internal/fromproto5/moveresourcestate_test.go @@ -165,8 +165,6 @@ func TestMoveResourceStateRequest(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/openephemeralresource.go b/internal/fromproto5/openephemeralresource.go new file mode 100644 index 000000000..a1bcbcc8a --- /dev/null +++ b/internal/fromproto5/openephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// OpenEphemeralResourceRequest returns the *fwserver.OpenEphemeralResourceRequest +// equivalent of a *tfprotov5.OpenEphemeralResourceRequest. +func OpenEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.OpenEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.OpenEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.OpenEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + ClientCapabilities: OpenEphemeralResourceClientCapabilities(proto5.ClientCapabilities), + } + + config, configDiags := Config(ctx, proto5.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + return fw, diags +} diff --git a/internal/fromproto5/openephemeralresource_test.go b/internal/fromproto5/openephemeralresource_test.go new file mode 100644 index 000000000..e84a5829c --- /dev/null +++ b/internal/fromproto5/openephemeralresource_test.go @@ -0,0 +1,144 @@ +// 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-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceRequest(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 := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.OpenEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.OpenEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.OpenEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.OpenEphemeralResourceRequest{ + Config: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "client-capabilities": { + input: &tfprotov5.OpenEphemeralResourceRequest{ + ClientCapabilities: &tfprotov5.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + EphemeralResourceSchema: testFwSchema, + ClientCapabilities: ephemeral.OpenClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + "client-capabilities-unset": { + input: &tfprotov5.OpenEphemeralResourceRequest{}, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + EphemeralResourceSchema: testFwSchema, + ClientCapabilities: ephemeral.OpenClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.OpenEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + 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/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_test.go b/internal/fromproto5/planresourcechange_test.go index 64dcad0f8..b223e0e59 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -262,8 +262,6 @@ func TestPlanResourceChangeRequest(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/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_test.go b/internal/fromproto5/readresource_test.go index fb19e9041..c58fc01cc 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -197,8 +197,6 @@ func TestReadResourceRequest(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/renewephemeralresource.go b/internal/fromproto5/renewephemeralresource.go new file mode 100644 index 000000000..c632310f4 --- /dev/null +++ b/internal/fromproto5/renewephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// RenewEphemeralResourceRequest returns the *fwserver.RenewEphemeralResourceRequest +// equivalent of a *tfprotov5.RenewEphemeralResourceRequest. +func RenewEphemeralResourceRequest(ctx context.Context, proto5 *tfprotov5.RenewEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.RenewEphemeralResourceRequest, diag.Diagnostics) { + if proto5 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.RenewEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + privateData, privateDataDiags := privatestate.NewData(ctx, proto5.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto5/renewephemeralresource_test.go b/internal/fromproto5/renewephemeralresource_test.go new file mode 100644 index 000000000..56b63d92a --- /dev/null +++ b/internal/fromproto5/renewephemeralresource_test.go @@ -0,0 +1,99 @@ +// 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-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov5.RenewEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.RenewEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.RenewEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov5.RenewEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.RenewEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); 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.go b/internal/fromproto5/validateephemeralresourceconfig.go new file mode 100644 index 000000000..ec1acb4b8 --- /dev/null +++ b/internal/fromproto5/validateephemeralresourceconfig.go @@ -0,0 +1,31 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ValidateEphemeralResourceConfigRequest returns the *fwserver.ValidateEphemeralResourceConfigRequest +// equivalent of a *tfprotov5.ValidateEphemeralResourceConfigRequest. +func ValidateEphemeralResourceConfigRequest(ctx context.Context, proto5 *tfprotov5.ValidateEphemeralResourceConfigRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.ValidateEphemeralResourceConfigRequest, diag.Diagnostics) { + if proto5 == nil { + return nil, nil + } + + fw := &fwserver.ValidateEphemeralResourceConfigRequest{} + + config, diags := Config(ctx, proto5.Config, ephemeralResourceSchema) + + fw.Config = config + fw.EphemeralResource = ephemeralResource + + return fw, diags +} diff --git a/internal/fromproto5/validateephemeralresourceconfig_test.go b/internal/fromproto5/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..b1ec6662a --- /dev/null +++ b/internal/fromproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,108 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateEphemeralResourceConfigRequest(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 := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ValidateEphemeralResourceConfigRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + expected *fwserver.ValidateEphemeralResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{}, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration 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"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testProto5DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ValidateEphemeralResourceConfigRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + 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/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_test.go b/internal/fromproto6/applyresourcechange_test.go index fccbf619a..9f845412e 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -250,8 +250,6 @@ func TestApplyResourceChangeRequest(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/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 6742a0303..d22d81623 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -75,3 +76,29 @@ func ImportStateClientCapabilities(in *tfprotov6.ImportResourceStateClientCapabi DeferralAllowed: in.DeferralAllowed, } } + +func OpenEphemeralResourceClientCapabilities(in *tfprotov6.OpenEphemeralResourceClientCapabilities) ephemeral.OpenClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return ephemeral.OpenClientCapabilities{ + DeferralAllowed: false, + } + } + + return ephemeral.OpenClientCapabilities{ + 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.go b/internal/fromproto6/closeephemeralresource.go new file mode 100644 index 000000000..c98050fd1 --- /dev/null +++ b/internal/fromproto6/closeephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// CloseEphemeralResourceRequest returns the *fwserver.CloseEphemeralResourceRequest +// equivalent of a *tfprotov6.CloseEphemeralResourceRequest. +func CloseEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.CloseEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.CloseEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.CloseEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto6/closeephemeralresource_test.go b/internal/fromproto6/closeephemeralresource_test.go new file mode 100644 index 000000000..aab1a56a3 --- /dev/null +++ b/internal/fromproto6/closeephemeralresource_test.go @@ -0,0 +1,99 @@ +// 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-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +func TestCloseEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov6.CloseEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.CloseEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.CloseEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov6.CloseEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.CloseEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.CloseEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); 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/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.go b/internal/fromproto6/ephemeral_result_data.go new file mode 100644 index 000000000..1fef00304 --- /dev/null +++ b/internal/fromproto6/ephemeral_result_data.go @@ -0,0 +1,53 @@ +// 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" +) + +// EphemeralResultData returns the *tfsdk.EphemeralResultData for a *tfprotov6.DynamicValue and +// fwschema.Schema. +func EphemeralResultData(ctx context.Context, proto6DynamicValue *tfprotov6.DynamicValue, schema fwschema.Schema) (*tfsdk.EphemeralResultData, 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 Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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"+ + "Missing schema.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto6DynamicValue, schema, fwschemadata.DataDescriptionEphemeralResultData) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.EphemeralResultData{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto6/ephemeral_result_data_test.go b/internal/fromproto6/ephemeral_result_data_test.go new file mode 100644 index 000000000..ac48f3f2b --- /dev/null +++ b/internal/fromproto6/ephemeral_result_data_test.go @@ -0,0 +1,120 @@ +// 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 TestEphemeralResultData(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{ + Required: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.DynamicValue + schema fwschema.Schema + expected *tfsdk.EphemeralResultData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "missing-schema": { + input: &testProto6DynamicValue, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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"+ + "Missing schema.", + ), + }, + }, + "invalid-schema": { + input: &testProto6DynamicValue, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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: &testProto6DynamicValue, + schema: testFwSchema, + expected: &tfsdk.EphemeralResultData{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.EphemeralResultData(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/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/importresourcestate_test.go b/internal/fromproto6/importresourcestate_test.go index 6c72913f7..6b385bb1a 100644 --- a/internal/fromproto6/importresourcestate_test.go +++ b/internal/fromproto6/importresourcestate_test.go @@ -119,8 +119,6 @@ func TestImportResourceStateRequest(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/moveresourcestate_test.go b/internal/fromproto6/moveresourcestate_test.go index 3379a3b6e..881ab3a49 100644 --- a/internal/fromproto6/moveresourcestate_test.go +++ b/internal/fromproto6/moveresourcestate_test.go @@ -165,8 +165,6 @@ func TestMoveResourceStateRequest(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/openephemeralresource.go b/internal/fromproto6/openephemeralresource.go new file mode 100644 index 000000000..196c6c0b0 --- /dev/null +++ b/internal/fromproto6/openephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// OpenEphemeralResourceRequest returns the *fwserver.OpenEphemeralResourceRequest +// equivalent of a *tfprotov6.OpenEphemeralResourceRequest. +func OpenEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.OpenEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.OpenEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.OpenEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + ClientCapabilities: OpenEphemeralResourceClientCapabilities(proto6.ClientCapabilities), + } + + config, configDiags := Config(ctx, proto6.Config, ephemeralResourceSchema) + + diags.Append(configDiags...) + + fw.Config = config + + return fw, diags +} diff --git a/internal/fromproto6/openephemeralresource_test.go b/internal/fromproto6/openephemeralresource_test.go new file mode 100644 index 000000000..a6535741e --- /dev/null +++ b/internal/fromproto6/openephemeralresource_test.go @@ -0,0 +1,144 @@ +// 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-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +func TestOpenEphemeralResourceRequest(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 := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.OpenEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.OpenEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.OpenEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "config-missing-schema": { + input: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.OpenEphemeralResourceRequest{ + Config: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + "client-capabilities": { + input: &tfprotov6.OpenEphemeralResourceRequest{ + ClientCapabilities: &tfprotov6.OpenEphemeralResourceClientCapabilities{ + DeferralAllowed: true, + }, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + EphemeralResourceSchema: testFwSchema, + ClientCapabilities: ephemeral.OpenClientCapabilities{ + DeferralAllowed: true, + }, + }, + }, + "client-capabilities-unset": { + input: &tfprotov6.OpenEphemeralResourceRequest{}, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.OpenEphemeralResourceRequest{ + EphemeralResourceSchema: testFwSchema, + ClientCapabilities: ephemeral.OpenClientCapabilities{ + DeferralAllowed: false, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.OpenEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + 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/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_test.go b/internal/fromproto6/planresourcechange_test.go index 81901c514..af8ee025c 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -262,8 +262,6 @@ func TestPlanResourceChangeRequest(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/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_test.go b/internal/fromproto6/readresource_test.go index 03d6eea86..5ae9e1688 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -197,8 +197,6 @@ func TestReadResourceRequest(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/renewephemeralresource.go b/internal/fromproto6/renewephemeralresource.go new file mode 100644 index 000000000..9ee02f7c1 --- /dev/null +++ b/internal/fromproto6/renewephemeralresource.go @@ -0,0 +1,52 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// RenewEphemeralResourceRequest returns the *fwserver.RenewEphemeralResourceRequest +// equivalent of a *tfprotov6.RenewEphemeralResourceRequest. +func RenewEphemeralResourceRequest(ctx context.Context, proto6 *tfprotov6.RenewEphemeralResourceRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.RenewEphemeralResourceRequest, diag.Diagnostics) { + if proto6 == 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 ephemeralResourceSchema == nil { + diags.AddError( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ) + + return nil, diags + } + + fw := &fwserver.RenewEphemeralResourceRequest{ + EphemeralResource: ephemeralResource, + EphemeralResourceSchema: ephemeralResourceSchema, + } + + privateData, privateDataDiags := privatestate.NewData(ctx, proto6.Private) + + diags.Append(privateDataDiags...) + + fw.Private = privateData + + return fw, diags +} diff --git a/internal/fromproto6/renewephemeralresource_test.go b/internal/fromproto6/renewephemeralresource_test.go new file mode 100644 index 000000000..0460abf1b --- /dev/null +++ b/internal/fromproto6/renewephemeralresource_test.go @@ -0,0 +1,99 @@ +// 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-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +func TestRenewEphemeralResourceRequest(t *testing.T) { + t.Parallel() + + testFwSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testCases := map[string]struct { + input *tfprotov6.RenewEphemeralResourceRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + providerMetaSchema fwschema.Schema + expected *fwserver.RenewEphemeralResourceRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.RenewEphemeralResourceRequest{}, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing EphemeralResource Schema", + "An unexpected error was encountered when handling the request. "+ + "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"+ + "Missing schema.", + ), + }, + }, + "private": { + input: &tfprotov6.RenewEphemeralResourceRequest{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.RenewEphemeralResourceRequest{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + }, + Provider: testProviderData, + }, + EphemeralResourceSchema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.RenewEphemeralResourceRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); 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.go b/internal/fromproto6/validateephemeralresourceconfig.go new file mode 100644 index 000000000..f913ede97 --- /dev/null +++ b/internal/fromproto6/validateephemeralresourceconfig.go @@ -0,0 +1,31 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ValidateEphemeralResourceConfigRequest returns the *fwserver.ValidateEphemeralResourceConfigRequest +// equivalent of a *tfprotov6.ValidateEphemeralResourceConfigRequest. +func ValidateEphemeralResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.ValidateEphemeralResourceConfigRequest, ephemeralResource ephemeral.EphemeralResource, ephemeralResourceSchema fwschema.Schema) (*fwserver.ValidateEphemeralResourceConfigRequest, diag.Diagnostics) { + if proto6 == nil { + return nil, nil + } + + fw := &fwserver.ValidateEphemeralResourceConfigRequest{} + + config, diags := Config(ctx, proto6.Config, ephemeralResourceSchema) + + fw.Config = config + fw.EphemeralResource = ephemeralResource + + return fw, diags +} diff --git a/internal/fromproto6/validateephemeralresourceconfig_test.go b/internal/fromproto6/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..093757252 --- /dev/null +++ b/internal/fromproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,108 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestValidateEphemeralResourceConfigRequest(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 := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ValidateEphemeralResourceConfigRequest + ephemeralResourceSchema fwschema.Schema + ephemeralResource ephemeral.EphemeralResource + expected *fwserver.ValidateEphemeralResourceConfigRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{}, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + }, + "config-missing-schema": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Configuration", + "An unexpected error was encountered when converting the configuration 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"+ + "Missing schema.", + ), + }, + }, + "config": { + input: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + ephemeralResourceSchema: testFwSchema, + expected: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ValidateEphemeralResourceConfigRequest(context.Background(), testCase.input, testCase.ephemeralResource, testCase.ephemeralResourceSchema) + + 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/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..6c440b319 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,13 @@ 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 } // AttributesEqual is a helper function to perform equality testing on two @@ -101,5 +109,9 @@ func AttributesEqual(a, b Attribute) bool { return false } + if a.IsWriteOnly() != b.IsWriteOnly() { + 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/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 c002e9883..70ae62c76 100644 --- a/internal/fwschemadata/data_description.go +++ b/internal/fwschemadata/data_description.go @@ -15,6 +15,10 @@ const ( // DataDescriptionState is used for Data that represents // a state-based value. DataDescriptionState DataDescription = "state" + + // DataDescriptionEphemeralResultData is used for Data that represents + // the result of an ephemeral operation. + DataDescriptionEphemeralResultData DataDescription = "ephemeral result data" ) // DataDescription is a human friendly type for Data. Used in error @@ -40,6 +44,8 @@ func (d DataDescription) Title() string { return "Plan" case DataDescriptionState: return "State" + case DataDescriptionEphemeralResultData: + return "Ephemeral Result Data" 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 5a0f90722..b6f2bc85e 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -33,6 +34,11 @@ type Server struct { // to [resource.ConfigureRequest.ProviderData]. ResourceConfigureData any + // EphemeralResourceConfigureData is the + // [provider.ConfigureResponse.EphemeralResourceData] field value which is passed + // to [ephemeral.ConfigureRequest.ProviderData]. + EphemeralResourceConfigureData any + // dataSourceSchemas is the cached DataSource Schemas for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the DataSourceType.GetSchema() method. @@ -56,6 +62,29 @@ type Server struct { // access from race conditions. dataSourceTypesMutex sync.Mutex + // ephemeralResourceSchemas is the cached EphemeralResource Schemas for RPCs that need to + // convert configuration data from the protocol. If not found, it will be + // fetched from the EphemeralResourceType.GetSchema() method. + ephemeralResourceSchemas map[string]fwschema.Schema + + // ephemeralResourceSchemasMutex is a mutex to protect concurrent ephemeralResourceSchemas + // access from race conditions. + ephemeralResourceSchemasMutex sync.RWMutex + + // ephemeralResourceFuncs is the cached EphemeralResource functions for RPCs that need to + // access ephemeral resources. If not found, it will be fetched from the + // Provider.EphemeralResources() method. + ephemeralResourceFuncs map[string]func() ephemeral.EphemeralResource + + // ephemeralResourceFuncsDiags is the cached Diagnostics obtained while populating + // ephemeralResourceFuncs. This is to ensure any warnings or errors are also + // returned appropriately when fetching ephemeralResourceFuncs. + ephemeralResourceFuncsDiags diag.Diagnostics + + // ephemeralResourceFuncsMutex is a mutex to protect concurrent ephemeralResourceFuncs + // access from race conditions. + ephemeralResourceFuncsMutex sync.Mutex + // deferred indicates an automatic provider deferral. When this is set, // the provider will automatically defer the PlanResourceChange, ReadResource, // ImportResourceState, and ReadDataSource RPCs. diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 59e10d76c..ea4452244 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -59,6 +59,31 @@ func TestServerApplyResourceChange(t *testing.T) { 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"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -398,6 +423,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{}, @@ -1259,6 +1331,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 +1478,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.go b/internal/fwserver/server_closeephemeralresource.go new file mode 100644 index 000000000..a3d4d68f6 --- /dev/null +++ b/internal/fwserver/server_closeephemeralresource.go @@ -0,0 +1,76 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +// CloseEphemeralResourceRequest is the framework server request for the +// CloseEphemeralResource RPC. +type CloseEphemeralResourceRequest struct { + Private *privatestate.Data + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// CloseEphemeralResourceResponse is the framework server response for the +// CloseEphemeralResource RPC. +type CloseEphemeralResourceResponse struct { + Diagnostics diag.Diagnostics +} + +// CloseEphemeralResource implements the framework server CloseEphemeralResource RPC. +func (s *Server) CloseEphemeralResource(ctx context.Context, req *CloseEphemeralResourceRequest, resp *CloseEphemeralResourceResponse) { + if req == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + resourceWithClose, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithClose) + if !ok { + // Terraform will always give the ephemeral resource an opportunity to close, so if it's not implemented we can safely return. + return + } + + privateProviderData := privatestate.EmptyProviderData(ctx) + if req.Private != nil && req.Private.Provider != nil { + privateProviderData = req.Private.Provider + } + + closeReq := ephemeral.CloseRequest{ + Private: privateProviderData, + } + closeResp := ephemeral.CloseResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Close") + resourceWithClose.Close(ctx, closeReq, &closeResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Close") + + resp.Diagnostics = closeResp.Diagnostics +} diff --git a/internal/fwserver/server_closeephemeralresource_test.go b/internal/fwserver/server_closeephemeralresource_test.go new file mode 100644 index 000000000..3ffc19c86 --- /dev/null +++ b/internal/fwserver/server_closeephemeralresource_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.CloseEphemeralResourceRequest + expectedResponse *fwserver.CloseEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + Private: testPrivate, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + var expected []byte + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "ephemeralresource-no-close-implementation": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + // Doesn't implement Close interface + EphemeralResource: &testprovider.EphemeralResource{}, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigureAndClose{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CloseEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithClose{ + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.CloseEphemeralResourceResponse{} + testCase.server.CloseEphemeralResource(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_configureprovider.go b/internal/fwserver/server_configureprovider.go index 2e04bc046..0b1807bce 100644 --- a/internal/fwserver/server_configureprovider.go +++ b/internal/fwserver/server_configureprovider.go @@ -37,4 +37,5 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR s.deferred = resp.Deferred s.DataSourceConfigureData = resp.DataSourceData s.ResourceConfigureData = resp.ResourceData + s.EphemeralResourceConfigureData = resp.EphemeralResourceData } diff --git a/internal/fwserver/server_configureprovider_test.go b/internal/fwserver/server_configureprovider_test.go index f7e29568e..96d5ac63a 100644 --- a/internal/fwserver/server_configureprovider_test.go +++ b/internal/fwserver/server_configureprovider_test.go @@ -178,6 +178,20 @@ func TestServerConfigureProvider(t *testing.T) { }, }, }, + "response-ephemeralresourcedata": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.EphemeralResourceData = "test-provider-configure-value" + }, + }, + }, + request: &provider.ConfigureRequest{}, + expectedResponse: &provider.ConfigureResponse{ + EphemeralResourceData: "test-provider-configure-value", + }, + }, "response-invalid-deferral-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -216,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() @@ -235,6 +247,10 @@ func TestServerConfigureProvider(t *testing.T) { if diff := cmp.Diff(testCase.server.ResourceConfigureData, testCase.expectedResponse.ResourceData); diff != "" { t.Errorf("unexpected server.ResourceConfigureData difference: %s", diff) } + + if diff := cmp.Diff(testCase.server.EphemeralResourceConfigureData, testCase.expectedResponse.EphemeralResourceData); diff != "" { + t.Errorf("unexpected server.EphemeralResourceConfigureData difference: %s", diff) + } }) } } diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 30c491690..d5a0aef2e 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -156,11 +156,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..86bedcb8f 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -33,6 +33,13 @@ func TestServerCreateResource(t *testing.T) { }, } + 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{ @@ -76,6 +83,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, @@ -91,6 +110,11 @@ func TestServerCreateResource(t *testing.T) { 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, @@ -506,6 +530,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 +605,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_test.go b/internal/fwserver/server_deleteresource_test.go index 95024d9a1..2042e2764 100644 --- a/internal/fwserver/server_deleteresource_test.go +++ b/internal/fwserver/server_deleteresource_test.go @@ -420,8 +420,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_ephemeralresources.go b/internal/fwserver/server_ephemeralresources.go new file mode 100644 index 000000000..15d54e118 --- /dev/null +++ b/internal/fwserver/server_ephemeralresources.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// EphemeralResource returns the EphemeralResource for a given type name. +func (s *Server) EphemeralResource(ctx context.Context, typeName string) (ephemeral.EphemeralResource, diag.Diagnostics) { + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + ephemeralResourceFunc, ok := ephemeralResourceFuncs[typeName] + + if !ok { + diags.AddError( + "Ephemeral Resource Type Not Found", + fmt.Sprintf("No ephemeral resource type named %q was found in the provider.", typeName), + ) + + return nil, diags + } + + return ephemeralResourceFunc(), diags +} + +// EphemeralResourceFuncs returns a map of EphemeralResource functions. The results are cached +// on first use. +func (s *Server) EphemeralResourceFuncs(ctx context.Context) (map[string]func() ephemeral.EphemeralResource, diag.Diagnostics) { + logging.FrameworkTrace(ctx, "Checking EphemeralResourceFuncs lock") + s.ephemeralResourceFuncsMutex.Lock() + defer s.ephemeralResourceFuncsMutex.Unlock() + + if s.ephemeralResourceFuncs != nil { + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags + } + + providerTypeName := s.ProviderTypeName(ctx) + s.ephemeralResourceFuncs = make(map[string]func() ephemeral.EphemeralResource) + + provider, ok := s.Provider.(provider.ProviderWithEphemeralResources) + + if !ok { + // Only ephemeral resource specific RPCs should return diagnostics about the + // provider not implementing ephemeral resources or missing ephemeral resources. + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags + } + + logging.FrameworkTrace(ctx, "Calling provider defined Provider EphemeralResources") + ephemeralResourceFuncsSlice := provider.EphemeralResources(ctx) + logging.FrameworkTrace(ctx, "Called provider defined Provider EphemeralResources") + + for _, ephemeralResourceFunc := range ephemeralResourceFuncsSlice { + ephemeralResource := ephemeralResourceFunc() + + ephemeralResourceTypeNameReq := ephemeral.MetadataRequest{ + ProviderTypeName: providerTypeName, + } + ephemeralResourceTypeNameResp := ephemeral.MetadataResponse{} + + ephemeralResource.Metadata(ctx, ephemeralResourceTypeNameReq, &ephemeralResourceTypeNameResp) + + if ephemeralResourceTypeNameResp.TypeName == "" { + s.ephemeralResourceFuncsDiags.AddError( + "Ephemeral Resource Type Name Missing", + fmt.Sprintf("The %T EphemeralResource returned an empty string from the Metadata method. ", ephemeralResource)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found ephemeral resource type", map[string]interface{}{logging.KeyEphemeralResourceType: ephemeralResourceTypeNameResp.TypeName}) + + if _, ok := s.ephemeralResourceFuncs[ephemeralResourceTypeNameResp.TypeName]; ok { + s.ephemeralResourceFuncsDiags.AddError( + "Duplicate Ephemeral Resource Type Defined", + fmt.Sprintf("The %s ephemeral resource type name was returned for multiple ephemeral resources. ", ephemeralResourceTypeNameResp.TypeName)+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + s.ephemeralResourceFuncs[ephemeralResourceTypeNameResp.TypeName] = ephemeralResourceFunc + } + + return s.ephemeralResourceFuncs, s.ephemeralResourceFuncsDiags +} + +// EphemeralResourceMetadatas returns a slice of EphemeralResourceMetadata for the GetMetadata +// RPC. +func (s *Server) EphemeralResourceMetadatas(ctx context.Context) ([]EphemeralResourceMetadata, diag.Diagnostics) { + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + ephemeralResourceMetadatas := make([]EphemeralResourceMetadata, 0, len(ephemeralResourceFuncs)) + + for typeName := range ephemeralResourceFuncs { + ephemeralResourceMetadatas = append(ephemeralResourceMetadatas, EphemeralResourceMetadata{ + TypeName: typeName, + }) + } + + return ephemeralResourceMetadatas, diags +} + +// EphemeralResourceSchema returns the EphemeralResource Schema for the given type name and +// caches the result for later EphemeralResource operations. +func (s *Server) EphemeralResourceSchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { + s.ephemeralResourceSchemasMutex.RLock() + ephemeralResourceSchema, ok := s.ephemeralResourceSchemas[typeName] + s.ephemeralResourceSchemasMutex.RUnlock() + + if ok { + return ephemeralResourceSchema, nil + } + + var diags diag.Diagnostics + + ephemeralResource, ephemeralResourceDiags := s.EphemeralResource(ctx, typeName) + + diags.Append(ephemeralResourceDiags...) + + if diags.HasError() { + return nil, diags + } + + schemaReq := ephemeral.SchemaRequest{} + schemaResp := ephemeral.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Schema method", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + ephemeralResource.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Schema method", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + + if diags.HasError() { + return schemaResp.Schema, diags + } + + s.ephemeralResourceSchemasMutex.Lock() + + if s.ephemeralResourceSchemas == nil { + s.ephemeralResourceSchemas = make(map[string]fwschema.Schema) + } + + s.ephemeralResourceSchemas[typeName] = schemaResp.Schema + + s.ephemeralResourceSchemasMutex.Unlock() + + return schemaResp.Schema, diags +} + +// EphemeralResourceSchemas returns a map of EphemeralResource Schemas for the +// GetProviderSchema 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) EphemeralResourceSchemas(ctx context.Context) (map[string]fwschema.Schema, diag.Diagnostics) { + ephemeralResourceSchemas := make(map[string]fwschema.Schema) + + ephemeralResourceFuncs, diags := s.EphemeralResourceFuncs(ctx) + + for typeName, ephemeralResourceFunc := range ephemeralResourceFuncs { + ephemeralResource := ephemeralResourceFunc() + + schemaReq := ephemeral.SchemaRequest{} + schemaResp := ephemeral.SchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Schema", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + ephemeralResource.Schema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Schema", map[string]interface{}{logging.KeyEphemeralResourceType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + + if schemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := schemaResp.Schema.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + ephemeralResourceSchemas[typeName] = schemaResp.Schema + } + + return ephemeralResourceSchemas, diags +} 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.go b/internal/fwserver/server_getmetadata.go index ebd0728a9..458694f2b 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -18,6 +18,7 @@ type GetMetadataRequest struct{} type GetMetadataResponse struct { DataSources []DataSourceMetadata Diagnostics diag.Diagnostics + EphemeralResources []EphemeralResourceMetadata Functions []FunctionMetadata Resources []ResourceMetadata ServerCapabilities *ServerCapabilities @@ -30,6 +31,13 @@ type DataSourceMetadata struct { TypeName string } +// EphemeralResourceMetadata is the framework server equivalent of the +// tfprotov5.EphemeralResourceMetadata and tfprotov6.EphemeralResourceMetadata types. +type EphemeralResourceMetadata struct { + // TypeName is the name of the ephemeral resource. + TypeName string +} + // FunctionMetadata is the framework server equivalent of the // tfprotov5.FunctionMetadata and tfprotov6.FunctionMetadata types. type FunctionMetadata struct { @@ -47,6 +55,7 @@ type ResourceMetadata struct { // GetMetadata implements the framework server GetMetadata RPC. func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) { resp.DataSources = []DataSourceMetadata{} + resp.EphemeralResources = []EphemeralResourceMetadata{} resp.Functions = []FunctionMetadata{} resp.Resources = []ResourceMetadata{} resp.ServerCapabilities = s.ServerCapabilities() @@ -55,6 +64,10 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.Diagnostics.Append(diags...) + ephemeralResourceMetadatas, diags := s.EphemeralResourceMetadatas(ctx) + + resp.Diagnostics.Append(diags...) + functionMetadatas, diags := s.FunctionMetadatas(ctx) resp.Diagnostics.Append(diags...) @@ -68,6 +81,7 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp } resp.DataSources = datasourceMetadatas + resp.EphemeralResources = ephemeralResourceMetadatas resp.Functions = functionMetadatas resp.Resources = resourceMetadatas } diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 09461f601..8729f6dbd 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -32,9 +33,10 @@ func TestServerGetMetadata(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -75,8 +77,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -109,7 +112,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Data Source Type Defined", @@ -145,7 +149,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Data Source Type Name Missing", @@ -188,6 +193,166 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "testprovidertype_data_source", }, }, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Ephemeral Resource Type Defined", + "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. "+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Type Name Missing", + "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "testprovidertype_ephemeral_resource", + }, + }, Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -222,7 +387,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{ { Name: "function1", @@ -264,7 +430,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Function Name Defined", @@ -300,7 +467,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Function Name Missing", @@ -342,8 +510,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "test_resource1", @@ -384,7 +553,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Duplicate Resource Type Defined", @@ -420,7 +590,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( "Resource Type Name Missing", @@ -458,8 +629,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ - DataSources: []fwserver.DataSourceMetadata{}, - Functions: []fwserver.FunctionMetadata{}, + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "testprovidertype_resource", @@ -475,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() @@ -488,6 +658,10 @@ func TestServerGetMetadata(t *testing.T) { return response.DataSources[i].TypeName < response.DataSources[j].TypeName }) + sort.Slice(response.EphemeralResources, func(i int, j int) bool { + return response.EphemeralResources[i].TypeName < response.EphemeralResources[j].TypeName + }) + sort.Slice(response.Functions, func(i int, j int) bool { return response.Functions[i].Name < response.Functions[j].Name }) diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index afcca8352..b8061dd10 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -18,13 +18,14 @@ type GetProviderSchemaRequest struct{} // GetProviderSchemaResponse is the framework server response for the // GetProviderSchema RPC. type GetProviderSchemaResponse struct { - ServerCapabilities *ServerCapabilities - Provider fwschema.Schema - ProviderMeta fwschema.Schema - ResourceSchemas map[string]fwschema.Schema - DataSourceSchemas map[string]fwschema.Schema - FunctionDefinitions map[string]function.Definition - Diagnostics diag.Diagnostics + ServerCapabilities *ServerCapabilities + Provider fwschema.Schema + ProviderMeta fwschema.Schema + ResourceSchemas map[string]fwschema.Schema + DataSourceSchemas map[string]fwschema.Schema + EphemeralResourceSchemas map[string]fwschema.Schema + FunctionDefinitions map[string]function.Definition + Diagnostics diag.Diagnostics } // GetProviderSchema implements the framework server GetProviderSchema RPC. @@ -80,4 +81,14 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe } resp.FunctionDefinitions = functions + + ephemeralResourceSchemas, diags := s.EphemeralResourceSchemas(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.EphemeralResourceSchemas = ephemeralResourceSchemas } diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index 3c975d11b..e5e17bc89 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -12,6 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -36,10 +38,11 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -106,9 +109,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -312,6 +316,290 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + "test_ephemeral_resource2": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "$": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + 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 (_).", + ), + }, + }, + }, + "ephemeralschema-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Ephemeral Resource Type Defined", + "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. "+ + "Ephemeral resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: nil, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Type Name Missing", + "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschema-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "testprovidertype_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + }, FunctionDefinitions: map[string]function.Definition{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, @@ -357,7 +645,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{ "function1": { Return: function.StringReturn{}, @@ -535,8 +824,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, Provider: providerschema.Schema{ Attributes: map[string]providerschema.Attribute{ "test": providerschema.StringAttribute{ @@ -601,9 +891,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ProviderMeta: metaschema.Schema{ Attributes: map[string]metaschema.Attribute{ "test": metaschema.StringAttribute{ @@ -696,9 +987,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "test_resource1": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ @@ -908,9 +1200,10 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - FunctionDefinitions: map[string]function.Definition{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "testprovidertype_resource": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ @@ -930,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_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 7288cc3e7..4abe204cd 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -142,6 +142,18 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + // 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 importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index bd0275a12..0f8481eea 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -33,12 +33,26 @@ func TestServerImportResourceState(t *testing.T) { }, } + 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{ @@ -61,11 +75,31 @@ func TestServerImportResourceState(t *testing.T) { }, } + 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, @@ -416,11 +450,42 @@ 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, + }, + }, + }, + }, } 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..1a832f3fe 100644 --- a/internal/fwserver/server_moveresourcestate.go +++ b/internal/fwserver/server_moveresourcestate.go @@ -205,6 +205,18 @@ 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 diff --git a/internal/fwserver/server_moveresourcestate_test.go b/internal/fwserver/server_moveresourcestate_test.go index 1a8e0fb77..8e2445ba9 100644 --- a/internal/fwserver/server_moveresourcestate_test.go +++ b/internal/fwserver/server_moveresourcestate_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/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -18,7 +20,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) { @@ -40,6 +41,22 @@ func TestServerMoveResourceState(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, + }, + }, + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.MoveResourceStateRequest @@ -757,11 +774,47 @@ 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, + }, + }, + }, } 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.go b/internal/fwserver/server_openephemeralresource.go new file mode 100644 index 000000000..18dc09bcf --- /dev/null +++ b/internal/fwserver/server_openephemeralresource.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "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/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// OpenEphemeralResourceRequest is the framework server request for the +// OpenEphemeralResource RPC. +type OpenEphemeralResourceRequest struct { + ClientCapabilities ephemeral.OpenClientCapabilities + Config *tfsdk.Config + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// OpenEphemeralResourceResponse is the framework server response for the +// OpenEphemeralResource RPC. +type OpenEphemeralResourceResponse struct { + Result *tfsdk.EphemeralResultData + Private *privatestate.Data + Diagnostics diag.Diagnostics + RenewAt time.Time + Deferred *ephemeral.Deferred +} + +// OpenEphemeralResource implements the framework server OpenEphemeralResource RPC. +func (s *Server) OpenEphemeralResource(ctx context.Context, req *OpenEphemeralResourceRequest, resp *OpenEphemeralResourceResponse) { + if req == nil { + return + } + + if s.deferred != nil { + logging.FrameworkDebug(ctx, "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.deferred.Reason.String(), + }, + ) + // Send an unknown value for the ephemeral resource. This will replace any configured values + // for ease of implementation as Terraform Core currently does not use these values for + // deferred actions, but this design could change in the future. + resp.Result = &tfsdk.EphemeralResultData{ + Raw: tftypes.NewValue(req.EphemeralResourceSchema.Type().TerraformType(ctx), tftypes.UnknownValue), + Schema: req.EphemeralResourceSchema, + } + resp.Deferred = &ephemeral.Deferred{ + Reason: ephemeral.DeferredReason(s.deferred.Reason), + } + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + openReq := ephemeral.OpenRequest{ + ClientCapabilities: req.ClientCapabilities, + Config: tfsdk.Config{ + Schema: req.EphemeralResourceSchema, + }, + } + openResp := ephemeral.OpenResponse{ + Result: tfsdk.EphemeralResultData{ + Schema: req.EphemeralResourceSchema, + }, + Private: privatestate.EmptyProviderData(ctx), + } + + if req.Config != nil { + openReq.Config = *req.Config + openResp.Result.Raw = req.Config.Raw.Copy() + } + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Open") + req.EphemeralResource.Open(ctx, openReq, &openResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Open") + + resp.Diagnostics = openResp.Diagnostics + resp.Result = &openResp.Result + resp.RenewAt = openResp.RenewAt + resp.Deferred = openResp.Deferred + + resp.Private = privatestate.EmptyData(ctx) + if openResp.Private != nil { + resp.Private.Provider = openResp.Private + } +} diff --git a/internal/fwserver/server_openephemeralresource_test.go b/internal/fwserver/server_openephemeralresource_test.go new file mode 100644 index 000000000..9f0365a92 --- /dev/null +++ b/internal/fwserver/server_openephemeralresource_test.go @@ -0,0 +1,400 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "fmt" + "testing" + "time" + + "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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testResultValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-result-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testResultUnknownValue := tftypes.NewValue(testType, tftypes.UnknownValue) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := &tfsdk.Config{ + Raw: testConfigValue, + Schema: testSchema, + } + + testResultUnchanged := &tfsdk.EphemeralResultData{ + Raw: testConfigValue, + Schema: testSchema, + } + + testResultUnknown := &tfsdk.EphemeralResultData{ + Raw: testResultUnknownValue, + Schema: testSchema, + } + + testResult := &tfsdk.EphemeralResultData{ + Raw: testResultValue, + Schema: testSchema, + } + + testDeferralAllowed := ephemeral.OpenClientCapabilities{ + DeferralAllowed: true, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.OpenEphemeralResourceRequest + expectedResponse *fwserver.OpenEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{}, + }, + "request-client-capabilities-deferral-allowed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + ClientCapabilities: testDeferralAllowed, + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + if req.ClientCapabilities.DeferralAllowed != true { + resp.Diagnostics.AddError("Unexpected req.ClientCapabilities.DeferralAllowed value", + "expected: true but got: false") + } + + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + }, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + }, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigure{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + }, + }, + "response-default-values": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {}, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + RenewAt: *new(time.Time), + }, + }, + "response-deferral-automatic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {}, + ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.Deferred = &provider.Deferred{Reason: provider.DeferredReasonProviderConfigUnknown} + }, + }, + }, + configureProviderReq: &provider.ConfigureRequest{ + ClientCapabilities: provider.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddError("Test assertion failed: ", "open shouldn't be called") + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnknown, + Deferred: &ephemeral.Deferred{Reason: ephemeral.DeferredReasonProviderConfigUnknown}, + }, + }, + "response-deferral-manual": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + resp.Deferred = &ephemeral.Deferred{Reason: ephemeral.DeferredReasonAbsentPrereq} + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + }, + ClientCapabilities: testDeferralAllowed, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + Deferred: &ephemeral.Deferred{Reason: ephemeral.DeferredReasonAbsentPrereq}, + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + Result: testResultUnchanged, + Private: testEmptyPrivate, + }, + }, + "response-renew-at": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testEmptyPrivate, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-result": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-result-value") + + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResult, + Private: testEmptyPrivate, + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.OpenEphemeralResourceRequest{ + Config: testConfig, + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResource{ + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, + }, + expectedResponse: &fwserver.OpenEphemeralResourceResponse{ + Result: testResultUnchanged, + Private: testPrivateProvider, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.OpenEphemeralResourceResponse{} + testCase.server.OpenEphemeralResource(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_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index d66fc4897..52b06688b 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -155,6 +155,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. @@ -337,6 +349,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..f1607daeb 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -405,7 +405,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 +437,14 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + 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 +574,21 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + 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 +1092,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, @@ -1368,6 +1395,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{}, @@ -3884,6 +3948,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{}, @@ -6343,8 +6451,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 +16064,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..d260e2912 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -6,6 +6,8 @@ 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/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -157,11 +159,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..9f3aa7065 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -34,11 +34,23 @@ func TestServerReadResource(t *testing.T) { }, } + 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"), }) + 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"), @@ -55,6 +67,18 @@ func TestServerReadResource(t *testing.T) { }, } + 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,6 +121,11 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testCurrentStateWriteOnly := &tfsdk.State{ + Raw: testCurrentStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testNewState := &tfsdk.State{ Raw: testNewStateValue, Schema: testSchema, @@ -562,6 +591,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 +665,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.go b/internal/fwserver/server_renewephemeralresource.go new file mode 100644 index 000000000..d23308de6 --- /dev/null +++ b/internal/fwserver/server_renewephemeralresource.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" +) + +// RenewEphemeralResourceRequest is the framework server request for the +// RenewEphemeralResource RPC. +type RenewEphemeralResourceRequest struct { + Private *privatestate.Data + EphemeralResourceSchema fwschema.Schema + EphemeralResource ephemeral.EphemeralResource +} + +// RenewEphemeralResourceResponse is the framework server response for the +// RenewEphemeralResource RPC. +type RenewEphemeralResourceResponse struct { + Private *privatestate.Data + Diagnostics diag.Diagnostics + RenewAt time.Time +} + +// RenewEphemeralResource implements the framework server RenewEphemeralResource RPC. +func (s *Server) RenewEphemeralResource(ctx context.Context, req *RenewEphemeralResourceRequest, resp *RenewEphemeralResourceResponse) { + if req == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + resourceWithRenew, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithRenew) + if !ok { + resp.Diagnostics.AddError( + "Ephemeral Resource Renew Not Implemented", + "An unexpected error was encountered when renewing the ephemeral resource. Terraform sent a renewal request for an "+ + "ephemeral resource that has not implemented renewal logic.\n\n"+ + "Please report this to the provider developer.", + ) + return + } + + // Ensure that resp.Private is never nil. + resp.Private = privatestate.EmptyData(ctx) + if req.Private != nil { + // Overwrite resp.Private with req.Private providing it is not nil. + resp.Private = req.Private + + // Ensure that resp.Private.Provider is never nil. + if resp.Private.Provider == nil { + resp.Private.Provider = privatestate.EmptyProviderData(ctx) + } + } + + renewReq := ephemeral.RenewRequest{ + Private: resp.Private.Provider, + } + renewResp := ephemeral.RenewResponse{ + Private: renewReq.Private, + } + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Renew") + resourceWithRenew.Renew(ctx, renewReq, &renewResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Renew") + + resp.Diagnostics = renewResp.Diagnostics + resp.RenewAt = renewResp.RenewAt + + if renewResp.Private != nil { + resp.Private.Provider = renewResp.Private + } +} diff --git a/internal/fwserver/server_renewephemeralresource_test.go b/internal/fwserver/server_renewephemeralresource_test.go new file mode 100644 index 000000000..8cc6e01a6 --- /dev/null +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -0,0 +1,281 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testPrivateFrameworkMap := map[string][]byte{ + ".frameworkKey": []byte(`{"fk": "framework value"}`), + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testPrivate := &privatestate.Data{ + Framework: testPrivateFrameworkMap, + Provider: testProviderData, + } + + testPrivateProvider := &privatestate.Data{ + Provider: testProviderData, + } + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEmptyPrivate := &privatestate.Data{ + Provider: testEmptyProviderData, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.RenewEphemeralResourceRequest + expectedResponse *fwserver.RenewEphemeralResourceResponse + configureProviderReq *provider.ConfigureRequest + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{}, + }, + "request-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + expected := `{"pKeyOne": {"k0": "zero", "k1": 1}}` + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if string(got) != expected { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + Private: testPrivate, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testPrivate, + }, + }, + "request-private-nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + var expected []byte + + key := "providerKeyOne" + got, diags := req.Private.GetKey(ctx, key) + + resp.Diagnostics.Append(diags...) + + if !bytes.Equal(got, expected) { + resp.Diagnostics.AddError("unexpected req.Private.Provider value: %s", string(got)) + } + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "ephemeralresource-no-renew-implementation-diagnostic": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + // Doesn't implement Renew interface + EphemeralResource: &testprovider.EphemeralResource{}, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Ephemeral Resource Renew Not Implemented", + "An unexpected error was encountered when renewing the ephemeral resource. Terraform sent a renewal request for an "+ + "ephemeral resource that has not implemented renewal logic.\n\n"+ + "Please report this to the provider developer.", + ), + }, + }, + }, + "ephemeralresource-configure-data": { + server: &fwserver.Server{ + EphemeralResourceConfigureData: "test-provider-configure-value", + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithConfigureAndRenew{ + ConfigureMethod: func(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + providerData, ok := req.ProviderData.(string) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected string, got: %T", req.ProviderData), + ) + return + } + + if providerData != "test-provider-configure-value" { + resp.Diagnostics.AddError( + "Unexpected ConfigureRequest.ProviderData", + fmt.Sprintf("Expected test-provider-configure-value, got: %q", providerData), + ) + } + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + // In practice, the Configure method would save the + // provider data to the EphemeralResource implementation and + // use it here. The fact that Configure is able to + // read the data proves this can work. + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + }, + }, + "response-default-values": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + RenewAt: *new(time.Time), + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }, + Private: testEmptyPrivate, + }, + }, + "response-renew-at": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testEmptyPrivate, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-private": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.RenewEphemeralResourceRequest{ + EphemeralResourceSchema: testSchema, + EphemeralResource: &testprovider.EphemeralResourceWithRenew{ + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + diags := resp.Private.SetKey(ctx, "providerKeyOne", []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`)) + + resp.Diagnostics.Append(diags...) + }, + }, + }, + expectedResponse: &fwserver.RenewEphemeralResourceResponse{ + Private: testPrivateProvider, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if testCase.configureProviderReq != nil { + configureProviderResp := &provider.ConfigureResponse{} + testCase.server.ConfigureProvider(context.Background(), testCase.configureProviderReq, configureProviderResp) + } + + response := &fwserver.RenewEphemeralResourceResponse{} + testCase.server.RenewEphemeralResource(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_updateresource.go b/internal/fwserver/server_updateresource.go index 9112c35c2..ad1d9f998 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -169,11 +169,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..4396e93f1 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -34,6 +34,13 @@ func TestServerUpdateResource(t *testing.T) { }, } + 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{ @@ -77,6 +84,18 @@ 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"` @@ -87,6 +106,11 @@ func TestServerUpdateResource(t *testing.T) { 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, @@ -777,6 +801,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 +916,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 new file mode 100644 index 000000000..6956af068 --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -0,0 +1,119 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ValidateEphemeralResourceConfigRequest is the framework server request for the +// ValidateEphemeralResourceConfig RPC. +type ValidateEphemeralResourceConfigRequest struct { + Config *tfsdk.Config + EphemeralResource ephemeral.EphemeralResource +} + +// ValidateEphemeralResourceConfigResponse is the framework server response for the +// ValidateEphemeralResourceConfig RPC. +type ValidateEphemeralResourceConfigResponse struct { + Diagnostics diag.Diagnostics +} + +// ValidateEphemeralResourceConfig implements the framework server ValidateEphemeralResourceConfig RPC. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, req *ValidateEphemeralResourceConfigRequest, resp *ValidateEphemeralResourceConfigResponse) { + if req == nil || req.Config == nil { + return + } + + if ephemeralResourceWithConfigure, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigure); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigure") + + configureReq := ephemeral.ConfigureRequest{ + ProviderData: s.EphemeralResourceConfigureData, + } + configureResp := ephemeral.ConfigureResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource Configure") + ephemeralResourceWithConfigure.Configure(ctx, configureReq, &configureResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource Configure") + + resp.Diagnostics.Append(configureResp.Diagnostics...) + + if resp.Diagnostics.HasError() { + return + } + } + + vdscReq := ephemeral.ValidateConfigRequest{ + Config: *req.Config, + } + + if ephemeralResourceWithConfigValidators, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithConfigValidators); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithConfigValidators") + + for _, configValidator := range ephemeralResourceWithConfigValidators.ConfigValidators(ctx) { + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &ephemeral.ValidateConfigResponse{} + + logging.FrameworkTrace( + ctx, + "Calling provider defined EphemeralResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + configValidator.ValidateEphemeralResource(ctx, vdscReq, vdscResp) + logging.FrameworkTrace( + ctx, + "Called provider defined EphemeralResourceConfigValidator", + map[string]interface{}{ + logging.KeyDescription: configValidator.Description(ctx), + }, + ) + + resp.Diagnostics.Append(vdscResp.Diagnostics...) + } + } + + if ephemeralResourceWithValidateConfig, ok := req.EphemeralResource.(ephemeral.EphemeralResourceWithValidateConfig); ok { + logging.FrameworkTrace(ctx, "EphemeralResource implements EphemeralResourceWithValidateConfig") + + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + vdscResp := &ephemeral.ValidateConfigResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined EphemeralResource ValidateConfig") + ephemeralResourceWithValidateConfig.ValidateConfig(ctx, vdscReq, vdscResp) + logging.FrameworkTrace(ctx, "Called provider defined EphemeralResource ValidateConfig") + + 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{ + ClientCapabilities: schemaCapabilities, + Config: *req.Config, + } + // Instantiate a new response for each request to prevent validators + // from modifying or removing diagnostics. + validateSchemaResp := ValidateSchemaResponse{} + + SchemaValidate(ctx, req.Config.Schema, validateSchemaReq, &validateSchemaResp) + + resp.Diagnostics.Append(validateSchemaResp.Diagnostics...) +} diff --git a/internal/fwserver/server_validateephemeralresourceconfig_test.go b/internal/fwserver/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..0497cd7d1 --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,306 @@ +// 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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "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 TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testConfig := tfsdk.Config{ + Raw: testValue, + Schema: testSchema, + } + + testSchemaAttributeValidator := 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.ConfigValue.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.AttributeConfig", "expected test-value, got "+req.ConfigValue.ValueString()) + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidator := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidator, + } + + testSchemaAttributeValidatorError := 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) { + resp.Diagnostics.AddAttributeError(req.Path, "error summary", "error detail") + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorError := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorError, + } + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.ValidateEphemeralResourceConfigRequest + expectedResponse *fwserver.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-AttributeValidator": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfigAttributeValidator, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchemaAttributeValidator + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-AttributeValidator-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfigAttributeValidatorError, + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorError + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "error summary", + "error detail", + ), + }, + }, + }, + "request-config-EphemeralResourceWithConfigValidators": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithConfigValidators{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []ephemeral.ConfigValidator { + return []ephemeral.ConfigValidator{ + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-EphemeralResourceWithConfigValidators-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithConfigValidators{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []ephemeral.ConfigValidator { + return []ephemeral.ConfigValidator{ + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddError("error summary 1", "error detail 1") + }, + }, + &testprovider.EphemeralResourceConfigValidator{ + ValidateEphemeralResourceMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + // Intentionally set diagnostics instead of add/append. + // The framework should not overwrite existing diagnostics. + // Reference: https://github.com/hashicorp/terraform-plugin-framework-validators/pull/94 + resp.Diagnostics = diag.Diagnostics{ + diag.NewErrorDiagnostic("error summary 2", "error detail 2"), + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "error summary 1", + "error detail 1", + ), + diag.NewErrorDiagnostic( + "error summary 2", + "error detail 2", + ), + }}, + }, + "request-config-EphemeralResourceWithValidateConfig": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + var got types.String + + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("test"), &got)...) + + if resp.Diagnostics.HasError() { + return + } + + if got.ValueString() != "test-value" { + resp.Diagnostics.AddError("Incorrect req.Config", "expected test-value, got "+got.ValueString()) + } + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config-EphemeralResourceWithValidateConfig-diagnostic": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateEphemeralResourceConfigRequest{ + Config: &testConfig, + EphemeralResource: &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + expectedResponse: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "warning summary", + "warning detail", + ), + diag.NewErrorDiagnostic( + "error summary", + "error detail", + ), + }}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.ValidateEphemeralResourceConfigResponse{} + testCase.server.ValidateEphemeralResourceConfig(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_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_test.go b/internal/fwtype/static_collection_validation_test.go index e7ea5e722..8000bc91a 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() diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 312a839ac..1443710c9 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -18,6 +18,9 @@ const ( // The type of data source being operated on, such as "archive_file" KeyDataSourceType = "tf_data_source_type" + // The type of ephemeral resource being operated on, such as "random_password" + KeyEphemeralResourceType = "tf_ephemeral_resource_type" + // The Deferred reason for an RPC response KeyDeferredReason = "tf_deferred_reason" diff --git a/internal/privatestate/data.go b/internal/privatestate/data.go index 3e858026a..5493a9d97 100644 --- a/internal/privatestate/data.go +++ b/internal/privatestate/data.go @@ -72,9 +72,9 @@ func (d *Data) Bytes(ctx context.Context) ([]byte, diag.Diagnostics) { if !json.Valid(v) { diags.AddError( "Error Encoding Private State", - fmt.Sprintf("An error was encountered when validating private state value."+ + "An error was encountered when validating private state value."+ fmt.Sprintf("The value associated with key %q is is not valid JSON.\n\n", k)+ - "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer."), + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", ) tflog.Error(ctx, "error encoding private state: invalid JSON value", map[string]interface{}{"key": k, "value": v}) 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_test.go b/internal/proto5server/server_applyresourcechange_test.go index 92e0a1b24..54484786c 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -1416,8 +1416,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.go b/internal/proto5server/server_closeephemeralresource.go new file mode 100644 index 000000000..1e07cef33 --- /dev/null +++ b/internal/proto5server/server_closeephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// CloseEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) CloseEphemeralResource(ctx context.Context, proto5Req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CloseEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.CloseEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CloseEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.CloseEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_closeephemeralresource_test.go b/internal/proto5server/server_closeephemeralresource_test.go new file mode 100644 index 000000000..6f575786f --- /dev/null +++ b/internal/proto5server/server_closeephemeralresource_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.CloseEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.CloseEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CloseEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} 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 decc62739..9f8baf1fc 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -61,10 +62,12 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -96,7 +99,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -110,6 +114,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -134,7 +139,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -147,6 +153,138 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -178,7 +316,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ { Name: "function1", @@ -190,6 +329,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -221,7 +361,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -235,6 +376,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -259,7 +401,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -272,6 +415,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -303,8 +447,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource1", @@ -315,6 +460,7 @@ func TestServerGetMetadata(t *testing.T) { }, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -346,7 +492,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -360,6 +507,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -384,7 +532,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -397,6 +546,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -404,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() @@ -420,6 +568,10 @@ func TestServerGetMetadata(t *testing.T) { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.EphemeralResources, func(i int, j int) bool { + return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName + }) + sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name }) diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 29b33024a..590d87a62 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -11,6 +11,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -103,13 +105,15 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -159,7 +163,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -176,6 +181,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -200,7 +206,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -216,6 +223,199 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_ephemeral_resource2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -257,7 +457,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "function1": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -278,6 +479,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -319,7 +521,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -336,6 +539,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -360,7 +564,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -376,6 +581,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -398,8 +604,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -414,6 +621,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -437,8 +645,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -456,6 +665,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -505,8 +715,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -536,6 +747,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -585,7 +797,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -602,6 +815,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -626,7 +840,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -642,6 +857,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -649,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() @@ -774,6 +988,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Checking FunctionTypes lock", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking EphemeralResourceFuncs 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 EphemeralResources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto5server/server_importresourcestate_test.go b/internal/proto5server/server_importresourcestate_test.go index 8b6b3578e..268853c7d 100644 --- a/internal/proto5server/server_importresourcestate_test.go +++ b/internal/proto5server/server_importresourcestate_test.go @@ -233,8 +233,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_test.go b/internal/proto5server/server_moveresourcestate_test.go index b558b573c..5b5e53c97 100644 --- a/internal/proto5server/server_moveresourcestate_test.go +++ b/internal/proto5server/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/proto5server/server_openephemeralresource.go b/internal/proto5server/server_openephemeralresource.go new file mode 100644 index 000000000..b972bd4d8 --- /dev/null +++ b/internal/proto5server/server_openephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// OpenEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) OpenEphemeralResource(ctx context.Context, proto5Req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.OpenEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.OpenEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.OpenEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.OpenEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_openephemeralresource_test.go b/internal/proto5server/server_openephemeralresource_test.go new file mode 100644 index 000000000..85e8e911e --- /dev/null +++ b/internal/proto5server/server_openephemeralresource_test.go @@ -0,0 +1,266 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testResultDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-result-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.OpenEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.OpenEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Result: testEmptyDynamicValue, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Result: testConfigDynamicValue, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + Result: testConfigDynamicValue, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Result: testEmptyDynamicValue, + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "response-result": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-result-value") + + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.OpenEphemeralResourceResponse{ + Result: testResultDynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.OpenEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index ad16683fb..fc452f6af 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -1039,8 +1039,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_test.go b/internal/proto5server/server_readresource_test.go index 12a2604f3..8fbba4647 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -402,8 +402,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.go b/internal/proto5server/server_renewephemeralresource.go new file mode 100644 index 000000000..76be1f019 --- /dev/null +++ b/internal/proto5server/server_renewephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// RenewEphemeralResource satisfies the tfprotov5.ProviderServer interface. +func (s *Server) RenewEphemeralResource(ctx context.Context, proto5Req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.RenewEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.RenewEphemeralResourceRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.RenewEphemeralResource(ctx, fwReq, fwResp) + + return toproto5.RenewEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_renewephemeralresource_test.go b/internal/proto5server/server_renewephemeralresource_test.go new file mode 100644 index 000000000..651a7fc61 --- /dev/null +++ b/internal/proto5server/server_renewephemeralresource_test.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.RenewEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov5.RenewEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov5.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.RenewEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} 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.go b/internal/proto5server/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..04018a01b --- /dev/null +++ b/internal/proto5server/server_validateephemeralresourceconfig.go @@ -0,0 +1,50 @@ +// 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" +) + +// ValidateEphemeralResourceConfig satisfies the tfprotov5.ProviderServer interface. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, proto5Req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateEphemeralResourceConfigResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ValidateEphemeralResourceConfigRequest(ctx, proto5Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateEphemeralResourceConfig(ctx, fwReq, fwResp) + + return toproto5.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_validateephemeralresourceconfig_test.go b/internal/proto5server/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..7912087e4 --- /dev/null +++ b/internal/proto5server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov5.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov5.ValidateEphemeralResourceConfigRequest + expectedError error + expectedResponse *tfprotov5.ValidateEphemeralResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateEphemeralResourceConfig(context.Background(), testCase.request) + + 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) + } + }) + } +} 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_test.go b/internal/proto6server/server_applyresourcechange_test.go index 463371cb7..66c12933d 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -1416,8 +1416,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.go b/internal/proto6server/server_closeephemeralresource.go new file mode 100644 index 000000000..430ff2eaa --- /dev/null +++ b/internal/proto6server/server_closeephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// CloseEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) CloseEphemeralResource(ctx context.Context, proto6Req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CloseEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.CloseEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CloseEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.CloseEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_closeephemeralresource_test.go b/internal/proto6server/server_closeephemeralresource_test.go new file mode 100644 index 000000000..795343de1 --- /dev/null +++ b/internal/proto6server/server_closeephemeralresource_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestServerCloseEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.CloseEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.CloseEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.CloseEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithClose{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + CloseMethod: func(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CloseEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} 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 35dc1a4c7..b32933a65 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -61,10 +62,12 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -106,10 +109,12 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -143,10 +148,143 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource1", + }, + { + TypeName: "test_ephemeral_resource2", + }, + }, Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralresources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -178,7 +316,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ { Name: "function1", @@ -190,6 +329,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -221,7 +361,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -235,6 +376,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -259,7 +401,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -272,6 +415,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -303,8 +447,9 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource1", @@ -315,6 +460,7 @@ func TestServerGetMetadata(t *testing.T) { }, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -346,7 +492,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -360,6 +507,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -384,7 +532,8 @@ func TestServerGetMetadata(t *testing.T) { }, request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -397,6 +546,7 @@ func TestServerGetMetadata(t *testing.T) { Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -404,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() @@ -420,6 +568,10 @@ func TestServerGetMetadata(t *testing.T) { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.EphemeralResources, func(i int, j int) bool { + return got.EphemeralResources[i].TypeName < got.EphemeralResources[j].TypeName + }) + sort.Slice(got.Functions, func(i int, j int) bool { return got.Functions[i].Name < got.Functions[j].Name }) diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index fc5958f25..0b60159de 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -11,6 +11,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -103,13 +105,15 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -169,13 +173,15 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: map[string]*tfprotov6.Function{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -209,6 +215,157 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource1" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test1", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + "test_ephemeral_resource2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test2", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test1": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test2": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Ephemeral Resource Type Defined", + Detail: "The test_ephemeral_resource ephemeral resource type name was returned for multiple ephemeral resources. " + + "Ephemeral resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, @@ -216,6 +373,49 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "ephemeralschemas-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Ephemeral Resource Type Name Missing", + Detail: "The *testprovider.EphemeralResource EphemeralResource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -257,7 +457,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "function1": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -278,6 +479,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -319,7 +521,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -336,6 +539,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -360,7 +564,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -376,6 +581,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -398,8 +604,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -414,6 +621,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -437,8 +645,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -456,6 +665,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -505,8 +715,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -536,6 +747,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -585,7 +797,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -602,6 +815,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -626,7 +840,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Diagnostics: []*tfprotov6.Diagnostic{ { Severity: tfprotov6.DiagnosticSeverityError, @@ -642,6 +857,7 @@ func TestServerGetProviderSchema(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, PlanDestroy: true, }, }, @@ -649,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() @@ -774,6 +988,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Checking FunctionTypes lock", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking EphemeralResourceFuncs 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 EphemeralResources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider EphemeralResources", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto6server/server_importresourcestate_test.go b/internal/proto6server/server_importresourcestate_test.go index 024756df2..8c67646f0 100644 --- a/internal/proto6server/server_importresourcestate_test.go +++ b/internal/proto6server/server_importresourcestate_test.go @@ -233,8 +233,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_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.go b/internal/proto6server/server_openephemeralresource.go new file mode 100644 index 000000000..5ec9b2dff --- /dev/null +++ b/internal/proto6server/server_openephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// OpenEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) OpenEphemeralResource(ctx context.Context, proto6Req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.OpenEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.OpenEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.OpenEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.OpenEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_openephemeralresource_test.go b/internal/proto6server/server_openephemeralresource_test.go new file mode 100644 index 000000000..718dd4d3b --- /dev/null +++ b/internal/proto6server/server_openephemeralresource_test.go @@ -0,0 +1,266 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerOpenEphemeralResource(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_required": tftypes.String, + }, + } + + testConfigDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testEmptyDynamicValue := testNewDynamicValue(t, tftypes.Object{}, nil) + + testResultDynamicValue := testNewDynamicValue(t, testType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-result-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }) + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.OpenEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.OpenEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Result: testEmptyDynamicValue, + }, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if config.TestRequired.ValueString() != "test-config-value" { + resp.Diagnostics.AddError("unexpected req.Config value: %s", config.TestRequired.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Result: testConfigDynamicValue, + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + Result: testConfigDynamicValue, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testEmptyDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Result: testEmptyDynamicValue, + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "response-result": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + OpenMethod: func(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data struct { + TestComputed types.String `tfsdk:"test_computed"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + data.TestComputed = types.StringValue("test-result-value") + + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.OpenEphemeralResourceRequest{ + Config: testConfigDynamicValue, + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.OpenEphemeralResourceResponse{ + Result: testResultDynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.OpenEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 77bd54226..35b39c2e0 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -1038,8 +1038,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_test.go b/internal/proto6server/server_readresource_test.go index 140b4be7b..7e2180008 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -402,8 +402,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.go b/internal/proto6server/server_renewephemeralresource.go new file mode 100644 index 000000000..e60657d4f --- /dev/null +++ b/internal/proto6server/server_renewephemeralresource.go @@ -0,0 +1,50 @@ +// 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" +) + +// RenewEphemeralResource satisfies the tfprotov6.ProviderServer interface. +func (s *Server) RenewEphemeralResource(ctx context.Context, proto6Req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.RenewEphemeralResourceResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.RenewEphemeralResourceRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil + } + + s.FrameworkServer.RenewEphemeralResource(ctx, fwReq, fwResp) + + return toproto6.RenewEphemeralResourceResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_renewephemeralresource_test.go b/internal/proto6server/server_renewephemeralresource_test.go new file mode 100644 index 000000000..1ed95d505 --- /dev/null +++ b/internal/proto6server/server_renewephemeralresource_test.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestServerRenewEphemeralResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.RenewEphemeralResourceRequest + expectedError error + expectedResponse *tfprotov6.RenewEphemeralResourceResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {}, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "response-renew-at": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithRenew{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{} + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_ephemeral_resource" + }, + }, + RenewMethod: func(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + resp.RenewAt = time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.RenewEphemeralResourceRequest{ + TypeName: "test_ephemeral_resource", + }, + expectedResponse: &tfprotov6.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.RenewEphemeralResource(context.Background(), testCase.request) + + 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) + } + }) + } +} 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.go b/internal/proto6server/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..822860326 --- /dev/null +++ b/internal/proto6server/server_validateephemeralresourceconfig.go @@ -0,0 +1,50 @@ +// 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" +) + +// ValidateEphemeralResourceConfig satisfies the tfprotov6.ProviderServer interface. +func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, proto6Req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.ValidateEphemeralResourceConfigResponse{} + + ephemeralResource, diags := s.FrameworkServer.EphemeralResource(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + ephemeralResourceSchema, diags := s.FrameworkServer.EphemeralResourceSchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ValidateEphemeralResourceConfigRequest(ctx, proto6Req, ephemeralResource, ephemeralResourceSchema) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil + } + + s.FrameworkServer.ValidateEphemeralResourceConfig(ctx, fwReq, fwResp) + + return toproto6.ValidateEphemeralResourceConfigResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_validateephemeralresourceconfig_test.go b/internal/proto6server/server_validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..b6ca07e44 --- /dev/null +++ b/internal/proto6server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerValidateEphemeralResourceConfig(t *testing.T) { + t.Parallel() + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + } + + testValue := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testDynamicValue, err := tfprotov6.NewDynamicValue(testType, testValue) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + }, + }, + } + + testCases := map[string]struct { + server *Server + request *tfprotov6.ValidateEphemeralResourceConfigRequest + expectedError error + expectedResponse *tfprotov6.ValidateEphemeralResourceConfigResponse + }{ + "no-schema": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "request-config": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + EphemeralResourcesMethod: func(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &testprovider.EphemeralResourceWithValidateConfig{ + EphemeralResource: &testprovider.EphemeralResource{ + SchemaMethod: func(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ValidateConfigMethod: func(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics.AddWarning("warning summary", "warning detail") + resp.Diagnostics.AddError("error summary", "error detail") + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ValidateEphemeralResourceConfigRequest{ + Config: &testDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.ValidateEphemeralResourceConfig(context.Background(), testCase.request) + + 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) + } + }) + } +} 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/ephemeralresource.go b/internal/testing/testprovider/ephemeralresource.go new file mode 100644 index 000000000..a6b0ea6ae --- /dev/null +++ b/internal/testing/testprovider/ephemeralresource.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResource{} + +// Declarative ephemeral.EphemeralResource for unit testing. +type EphemeralResource struct { + // EphemeralResource interface methods + MetadataMethod func(context.Context, ephemeral.MetadataRequest, *ephemeral.MetadataResponse) + SchemaMethod func(context.Context, ephemeral.SchemaRequest, *ephemeral.SchemaResponse) + OpenMethod func(context.Context, ephemeral.OpenRequest, *ephemeral.OpenResponse) +} + +// Metadata satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + if r.MetadataMethod == nil { + return + } + + r.MetadataMethod(ctx, req, resp) +} + +// Schema satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + if r.SchemaMethod == nil { + return + } + + r.SchemaMethod(ctx, req, resp) +} + +// Open satisfies the ephemeral.EphemeralResource interface. +func (r *EphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + if r.OpenMethod == nil { + return + } + + r.OpenMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourceconfigvalidator.go b/internal/testing/testprovider/ephemeralresourceconfigvalidator.go new file mode 100644 index 000000000..afbe3c49a --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourceconfigvalidator.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +var _ ephemeral.ConfigValidator = &EphemeralResourceConfigValidator{} + +// Declarative ephemeral.ConfigValidator for unit testing. +type EphemeralResourceConfigValidator struct { + // EphemeralResourceConfigValidator interface methods + DescriptionMethod func(context.Context) string + MarkdownDescriptionMethod func(context.Context) string + ValidateEphemeralResourceMethod func(context.Context, ephemeral.ValidateConfigRequest, *ephemeral.ValidateConfigResponse) +} + +// Description satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) Description(ctx context.Context) string { + if v.DescriptionMethod == nil { + return "" + } + + return v.DescriptionMethod(ctx) +} + +// MarkdownDescription satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) MarkdownDescription(ctx context.Context) string { + if v.MarkdownDescriptionMethod == nil { + return "" + } + + return v.MarkdownDescriptionMethod(ctx) +} + +// Validate satisfies the ephemeral.ConfigValidator interface. +func (v *EphemeralResourceConfigValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + if v.ValidateEphemeralResourceMethod == nil { + return + } + + v.ValidateEphemeralResourceMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithclose.go b/internal/testing/testprovider/ephemeralresourcewithclose.go new file mode 100644 index 000000000..545ac9aca --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithclose.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithClose{} +var _ ephemeral.EphemeralResourceWithClose = &EphemeralResourceWithClose{} + +// Declarative ephemeral.EphemeralResourceWithClose for unit testing. +type EphemeralResourceWithClose struct { + *EphemeralResource + + // EphemeralResourceWithClose interface methods + CloseMethod func(context.Context, ephemeral.CloseRequest, *ephemeral.CloseResponse) +} + +// Close satisfies the ephemeral.EphemeralResourceWithClose interface. +func (p *EphemeralResourceWithClose) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + if p.CloseMethod == nil { + return + } + + p.CloseMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigure.go b/internal/testing/testprovider/ephemeralresourcewithconfigure.go new file mode 100644 index 000000000..60ae3d430 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigure.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigure{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigure{} + +// Declarative ephemeral.EphemeralResourceWithConfigure for unit testing. +type EphemeralResourceWithConfigure struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (d *EphemeralResourceWithConfigure) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if d.ConfigureMethod == nil { + return + } + + d.ConfigureMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go b/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go new file mode 100644 index 000000000..a991fbdfc --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigureandclose.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigureAndClose{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigureAndClose{} +var _ ephemeral.EphemeralResourceWithClose = &EphemeralResourceWithConfigureAndClose{} + +// Declarative ephemeral.EphemeralResourceWithConfigureAndClose for unit testing. +type EphemeralResourceWithConfigureAndClose struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) + + // EphemeralResourceWithClose interface methods + CloseMethod func(context.Context, ephemeral.CloseRequest, *ephemeral.CloseResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (r *EphemeralResourceWithConfigureAndClose) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// Close satisfies the ephemeral.EphemeralResourceWithClose interface. +func (r *EphemeralResourceWithConfigureAndClose) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + if r.CloseMethod == nil { + return + } + + r.CloseMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go b/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go new file mode 100644 index 000000000..d9feeb16d --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigureAndRenew{} +var _ ephemeral.EphemeralResourceWithConfigure = &EphemeralResourceWithConfigureAndRenew{} +var _ ephemeral.EphemeralResourceWithRenew = &EphemeralResourceWithConfigureAndRenew{} + +// Declarative ephemeral.EphemeralResourceWithConfigureAndRenew for unit testing. +type EphemeralResourceWithConfigureAndRenew struct { + *EphemeralResource + + // EphemeralResourceWithConfigure interface methods + ConfigureMethod func(context.Context, ephemeral.ConfigureRequest, *ephemeral.ConfigureResponse) + + // EphemeralResourceWithRenew interface methods + RenewMethod func(context.Context, ephemeral.RenewRequest, *ephemeral.RenewResponse) +} + +// Configure satisfies the ephemeral.EphemeralResourceWithConfigure interface. +func (r *EphemeralResourceWithConfigureAndRenew) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + if r.ConfigureMethod == nil { + return + } + + r.ConfigureMethod(ctx, req, resp) +} + +// Renew satisfies the ephemeral.EphemeralResourceWithRenew interface. +func (r *EphemeralResourceWithConfigureAndRenew) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + if r.RenewMethod == nil { + return + } + + r.RenewMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go b/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go new file mode 100644 index 000000000..824ceef78 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithconfigvalidators.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithConfigValidators{} +var _ ephemeral.EphemeralResourceWithConfigValidators = &EphemeralResourceWithConfigValidators{} + +// Declarative ephemeral.EphemeralResourceWithConfigValidators for unit testing. +type EphemeralResourceWithConfigValidators struct { + *EphemeralResource + + // EphemeralResourceWithConfigValidators interface methods + ConfigValidatorsMethod func(context.Context) []ephemeral.ConfigValidator +} + +// ConfigValidators satisfies the ephemeral.EphemeralResourceWithConfigValidators interface. +func (p *EphemeralResourceWithConfigValidators) ConfigValidators(ctx context.Context) []ephemeral.ConfigValidator { + if p.ConfigValidatorsMethod == nil { + return nil + } + + return p.ConfigValidatorsMethod(ctx) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithrenew.go b/internal/testing/testprovider/ephemeralresourcewithrenew.go new file mode 100644 index 000000000..174e80d76 --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithrenew.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithRenew{} +var _ ephemeral.EphemeralResourceWithRenew = &EphemeralResourceWithRenew{} + +// Declarative ephemeral.EphemeralResourceWithRenew for unit testing. +type EphemeralResourceWithRenew struct { + *EphemeralResource + + // EphemeralResourceWithRenew interface methods + RenewMethod func(context.Context, ephemeral.RenewRequest, *ephemeral.RenewResponse) +} + +// Renew satisfies the ephemeral.EphemeralResourceWithRenew interface. +func (p *EphemeralResourceWithRenew) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + if p.RenewMethod == nil { + return + } + + p.RenewMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go b/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go new file mode 100644 index 000000000..3c6ac4ffb --- /dev/null +++ b/internal/testing/testprovider/ephemeralresourcewithvalidateconfig.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/ephemeral" +) + +var _ ephemeral.EphemeralResource = &EphemeralResourceWithValidateConfig{} +var _ ephemeral.EphemeralResourceWithValidateConfig = &EphemeralResourceWithValidateConfig{} + +// Declarative ephemeral.EphemeralResourceWithValidateConfig for unit testing. +type EphemeralResourceWithValidateConfig struct { + *EphemeralResource + + // EphemeralResourceWithValidateConfig interface methods + ValidateConfigMethod func(context.Context, ephemeral.ValidateConfigRequest, *ephemeral.ValidateConfigResponse) +} + +// ValidateConfig satisfies the ephemeral.EphemeralResourceWithValidateConfig interface. +func (p *EphemeralResourceWithValidateConfig) ValidateConfig(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + if p.ValidateConfigMethod == nil { + return + } + + p.ValidateConfigMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index eb9ef7138..0b2c536da 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -16,11 +17,12 @@ var _ provider.Provider = &Provider{} // Declarative provider.Provider for unit testing. type Provider struct { // Provider interface methods - MetadataMethod func(context.Context, provider.MetadataRequest, *provider.MetadataResponse) - ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) - SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) - DataSourcesMethod func(context.Context) []func() datasource.DataSource - ResourcesMethod func(context.Context) []func() resource.Resource + MetadataMethod func(context.Context, provider.MetadataRequest, *provider.MetadataResponse) + ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) + SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) + DataSourcesMethod func(context.Context) []func() datasource.DataSource + ResourcesMethod func(context.Context) []func() resource.Resource + EphemeralResourcesMethod func(context.Context) []func() ephemeral.EphemeralResource } // Configure satisfies the provider.Provider interface. @@ -67,3 +69,11 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return p.ResourcesMethod(ctx) } + +func (p *Provider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource { + if p == nil || p.EphemeralResourcesMethod == nil { + return nil + } + + return p.EphemeralResourcesMethod(ctx) +} diff --git a/internal/testing/testschema/attribute.go b/internal/testing/testschema/attribute.go index fccc26b42..979db68eb 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,7 @@ type Attribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -77,3 +79,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithbooldefault.go b/internal/testing/testschema/attributewithbooldefault.go index 5d7b25671..66edc07e5 100644 --- a/internal/testing/testschema/attributewithbooldefault.go +++ b/internal/testing/testschema/attributewithbooldefault.go @@ -22,6 +22,7 @@ type AttributeWithBoolDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Bool } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go index d27661894..397f18186 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,7 @@ type AttributeWithBoolPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Bool } @@ -85,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go index 5cc0943ca..044c25cf4 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,7 @@ type AttributeWithBoolValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Bool } @@ -85,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithdynamicdefault.go b/internal/testing/testschema/attributewithdynamicdefault.go index d366beb9a..b562132a8 100644 --- a/internal/testing/testschema/attributewithdynamicdefault.go +++ b/internal/testing/testschema/attributewithdynamicdefault.go @@ -22,6 +22,7 @@ type AttributeWithDynamicDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Dynamic } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithdynamicplanmodifiers.go b/internal/testing/testschema/attributewithdynamicplanmodifiers.go index abb7ca6bf..74a80587a 100644 --- a/internal/testing/testschema/attributewithdynamicplanmodifiers.go +++ b/internal/testing/testschema/attributewithdynamicplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithDynamicPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Dynamic } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithdynamicvalidators.go b/internal/testing/testschema/attributewithdynamicvalidators.go index 1fe086775..e4ef1024e 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,7 @@ type AttributeWithDynamicValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Dynamic } @@ -85,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go index e1dabdd87..c3aeb627d 100644 --- a/internal/testing/testschema/attributewithfloat32default.go +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -22,6 +22,7 @@ type AttributeWithFloat32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float32 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go index b3ef10ebc..f93d87229 100644 --- a/internal/testing/testschema/attributewithfloat32planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithFloat32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float32 } @@ -86,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go index d9d38f9ea..7fb02a5ad 100644 --- a/internal/testing/testschema/attributewithfloat32validators.go +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -23,6 +23,7 @@ type AttributeWithFloat32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float32 } @@ -86,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat64default.go b/internal/testing/testschema/attributewithfloat64default.go index 33b717f33..484ec37d9 100644 --- a/internal/testing/testschema/attributewithfloat64default.go +++ b/internal/testing/testschema/attributewithfloat64default.go @@ -22,6 +22,7 @@ type AttributeWithFloat64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go index 6273cbd5d..ba04291da 100644 --- a/internal/testing/testschema/attributewithfloat64planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithFloat64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go index b2b2b5f70..02ef17704 100644 --- a/internal/testing/testschema/attributewithfloat64validators.go +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -22,6 +22,7 @@ type AttributeWithFloat64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go index bbfe22af8..f332bae41 100644 --- a/internal/testing/testschema/attributewithint32default.go +++ b/internal/testing/testschema/attributewithint32default.go @@ -22,6 +22,7 @@ type AttributeWithInt32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int32 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go index aff453d9c..7f131df58 100644 --- a/internal/testing/testschema/attributewithint32planmodifiers.go +++ b/internal/testing/testschema/attributewithint32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithInt32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int32 } @@ -86,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go index 7c6913bcc..8a4546e9e 100644 --- a/internal/testing/testschema/attributewithint32validators.go +++ b/internal/testing/testschema/attributewithint32validators.go @@ -23,6 +23,7 @@ type AttributeWithInt32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int32 } @@ -86,3 +87,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint64default.go b/internal/testing/testschema/attributewithint64default.go index ca9e12b96..574a88a58 100644 --- a/internal/testing/testschema/attributewithint64default.go +++ b/internal/testing/testschema/attributewithint64default.go @@ -22,6 +22,7 @@ type AttributeWithInt64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go index 368a865c0..e21a43251 100644 --- a/internal/testing/testschema/attributewithint64planmodifiers.go +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithInt64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go index 07cf28bd7..c4e23e166 100644 --- a/internal/testing/testschema/attributewithint64validators.go +++ b/internal/testing/testschema/attributewithint64validators.go @@ -22,6 +22,7 @@ type AttributeWithInt64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int64 } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithlistdefault.go b/internal/testing/testschema/attributewithlistdefault.go index 1f6c65d5a..ff23c5215 100644 --- a/internal/testing/testschema/attributewithlistdefault.go +++ b/internal/testing/testschema/attributewithlistdefault.go @@ -23,6 +23,7 @@ type AttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.List } @@ -88,3 +89,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go index 08ed953fa..fbb50c332 100644 --- a/internal/testing/testschema/attributewithlistplanmodifiers.go +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithListPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.List } @@ -84,6 +85,11 @@ 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 diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go index bb47ca9d6..fefa2eb02 100644 --- a/internal/testing/testschema/attributewithlistvalidators.go +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -23,6 +23,7 @@ type AttributeWithListValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.List } @@ -84,6 +85,11 @@ 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 diff --git a/internal/testing/testschema/attributewithmapdefault.go b/internal/testing/testschema/attributewithmapdefault.go index a8bf1910d..2b223cd1d 100644 --- a/internal/testing/testschema/attributewithmapdefault.go +++ b/internal/testing/testschema/attributewithmapdefault.go @@ -23,6 +23,7 @@ type AttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Map } @@ -88,3 +89,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go index 1d067e1d4..06c7b2fe4 100644 --- a/internal/testing/testschema/attributewithmapplanmodifiers.go +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithMapPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Map } @@ -84,6 +85,11 @@ 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 diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go index 1469ceee2..5e3a9dac8 100644 --- a/internal/testing/testschema/attributewithmapvalidators.go +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -23,6 +23,7 @@ type AttributeWithMapValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Map } @@ -84,6 +85,11 @@ func (a AttributeWithMapValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..effa79507 100644 --- a/internal/testing/testschema/attributewithnumberdefault.go +++ b/internal/testing/testschema/attributewithnumberdefault.go @@ -22,6 +22,7 @@ type AttributeWithNumberDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Number } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go index 7073a1e72..cf9299778 100644 --- a/internal/testing/testschema/attributewithnumberplanmodifiers.go +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithNumberPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..af1869d38 100644 --- a/internal/testing/testschema/attributewithnumbervalidators.go +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -22,6 +22,7 @@ type AttributeWithNumberValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..ed25e0a90 100644 --- a/internal/testing/testschema/attributewithobjectdefault.go +++ b/internal/testing/testschema/attributewithobjectdefault.go @@ -23,6 +23,7 @@ type AttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Object } @@ -88,3 +89,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go index 87f5933e2..e4ec023bd 100644 --- a/internal/testing/testschema/attributewithobjectplanmodifiers.go +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithObjectPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..534c47cf6 100644 --- a/internal/testing/testschema/attributewithobjectvalidators.go +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -23,6 +23,7 @@ type AttributeWithObjectValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..f770f956c 100644 --- a/internal/testing/testschema/attributewithsetdefault.go +++ b/internal/testing/testschema/attributewithsetdefault.go @@ -23,6 +23,7 @@ type AttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Set } @@ -88,3 +89,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go index 4efb38ac4..a36f5adc4 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,7 @@ type AttributeWithSetPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Set } @@ -84,6 +86,11 @@ func (a AttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..32bc5256f 100644 --- a/internal/testing/testschema/attributewithsetvalidators.go +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -23,6 +23,7 @@ type AttributeWithSetValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Set } @@ -84,6 +85,11 @@ func (a AttributeWithSetValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..01e1e6f4e 100644 --- a/internal/testing/testschema/attributewithstringdefault.go +++ b/internal/testing/testschema/attributewithstringdefault.go @@ -22,6 +22,7 @@ type AttributeWithStringDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.String } @@ -85,3 +86,8 @@ 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 +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go index cbf324d66..d1b5af8d5 100644 --- a/internal/testing/testschema/attributewithstringplanmodifiers.go +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithStringPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..e1ddf7e59 100644 --- a/internal/testing/testschema/attributewithstringvalidators.go +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -22,6 +22,7 @@ type AttributeWithStringValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..f37301359 100644 --- a/internal/testing/testschema/nested_attribute.go +++ b/internal/testing/testschema/nested_attribute.go @@ -27,6 +27,7 @@ type NestedAttribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -157,3 +158,8 @@ 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 +} diff --git a/internal/testing/testschema/nested_attribute_with_list_default.go b/internal/testing/testschema/nested_attribute_with_list_default.go index 0980629cd..a1b70cd87 100644 --- a/internal/testing/testschema/nested_attribute_with_list_default.go +++ b/internal/testing/testschema/nested_attribute_with_list_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..2904c21ee 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,7 @@ type NestedAttributeWithListPlanModifiers struct { PlanModifiers []planmodifier.List Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..6eac9d3ff 100644 --- a/internal/testing/testschema/nested_attribute_with_map_default.go +++ b/internal/testing/testschema/nested_attribute_with_map_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..35f2a0732 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,7 @@ type NestedAttributeWithMapPlanModifiers struct { PlanModifiers []planmodifier.Map Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..e8579c2ac 100644 --- a/internal/testing/testschema/nested_attribute_with_object_default.go +++ b/internal/testing/testschema/nested_attribute_with_object_default.go @@ -28,6 +28,7 @@ type NestedAttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -105,6 +106,11 @@ func (a NestedAttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..2d86af989 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,7 @@ type NestedAttributeWithObjectPlanModifiers struct { PlanModifiers []planmodifier.Object Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -99,6 +100,11 @@ func (a NestedAttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..3a80c28a7 100644 --- a/internal/testing/testschema/nested_attribute_with_set_default.go +++ b/internal/testing/testschema/nested_attribute_with_set_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..1d886e70e 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,7 @@ type NestedAttributeWithSetPlanModifiers struct { PlanModifiers []planmodifier.Set Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // 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_test.go b/internal/toproto5/applyresourcechange_test.go index 64c84f7cc..85f8c99dd 100644 --- a/internal/toproto5/applyresourcechange_test.go +++ b/internal/toproto5/applyresourcechange_test.go @@ -158,8 +158,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.go b/internal/toproto5/closeephemeralresource.go new file mode 100644 index 000000000..5bc9484db --- /dev/null +++ b/internal/toproto5/closeephemeralresource.go @@ -0,0 +1,25 @@ +// 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" +) + +// CloseEphemeralResourceResponse returns the *tfprotov5.CloseEphemeralResourceResponse +// equivalent of a *fwserver.CloseEphemeralResourceResponse. +func CloseEphemeralResourceResponse(ctx context.Context, fw *fwserver.CloseEphemeralResourceResponse) *tfprotov5.CloseEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} diff --git a/internal/toproto5/closeephemeralresource_test.go b/internal/toproto5/closeephemeralresource_test.go new file mode 100644 index 000000000..12b2e1171 --- /dev/null +++ b/internal/toproto5/closeephemeralresource_test.go @@ -0,0 +1,67 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CloseEphemeralResourceResponse + expected *tfprotov5.CloseEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.CloseEphemeralResourceResponse{}, + expected: &tfprotov5.CloseEphemeralResourceResponse{}, + }, + "diagnostics": { + input: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.CloseEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.CloseEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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/deferred.go b/internal/toproto5/deferred.go index 3c5c4b1dc..42049a4da 100644 --- a/internal/toproto5/deferred.go +++ b/internal/toproto5/deferred.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -27,3 +28,12 @@ func ResourceDeferred(fw *resource.Deferred) *tfprotov5.Deferred { Reason: tfprotov5.DeferredReason(fw.Reason), } } + +func EphemeralResourceDeferred(fw *ephemeral.Deferred) *tfprotov5.Deferred { + if fw == nil { + return nil + } + return &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(fw.Reason), + } +} 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.go b/internal/toproto5/ephemeral_result_data.go new file mode 100644 index 000000000..96490446f --- /dev/null +++ b/internal/toproto5/ephemeral_result_data.go @@ -0,0 +1,28 @@ +// 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" +) + +// EphemeralResultData returns the *tfprotov5.DynamicValue for a *tfsdk.EphemeralResultData. +func EphemeralResultData(ctx context.Context, fw *tfsdk.EphemeralResultData) (*tfprotov5.DynamicValue, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + data := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralResultData, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + return DynamicValue(ctx, data) +} diff --git a/internal/toproto5/ephemeral_result_data_test.go b/internal/toproto5/ephemeral_result_data_test.go new file mode 100644 index 000000000..4a303f016 --- /dev/null +++ b/internal/toproto5/ephemeral_result_data_test.go @@ -0,0 +1,107 @@ +// 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 TestEphemeralResultData(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) + } + + testEphemeralResultData := &tfsdk.EphemeralResultData{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testEphemeralResultDataInvalid := &tfsdk.EphemeralResultData{ + 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.EphemeralResultData + expected *tfprotov5.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testEphemeralResultDataInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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: testEphemeralResultData, + expected: &testProto5DynamicValue, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto5.EphemeralResultData(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/ephemeralresourcemetadata.go b/internal/toproto5/ephemeralresourcemetadata.go new file mode 100644 index 000000000..e301fa35b --- /dev/null +++ b/internal/toproto5/ephemeralresourcemetadata.go @@ -0,0 +1,19 @@ +// 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" +) + +// EphemeralResourceMetadata returns the tfprotov5.EphemeralResourceMetadata for a +// fwserver.EphemeralResourceMetadata. +func EphemeralResourceMetadata(ctx context.Context, fw fwserver.EphemeralResourceMetadata) tfprotov5.EphemeralResourceMetadata { + return tfprotov5.EphemeralResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto5/ephemeralresourcemetadata_test.go b/internal/toproto5/ephemeralresourcemetadata_test.go new file mode 100644 index 000000000..81f6566d7 --- /dev/null +++ b/internal/toproto5/ephemeralresourcemetadata_test.go @@ -0,0 +1,44 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestEphemeralResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.EphemeralResourceMetadata + expected tfprotov5.EphemeralResourceMetadata + }{ + "TypeName": { + fw: fwserver.EphemeralResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov5.EphemeralResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.EphemeralResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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.go b/internal/toproto5/getmetadata.go index 9c1892d8a..4150c2473 100644 --- a/internal/toproto5/getmetadata.go +++ b/internal/toproto5/getmetadata.go @@ -17,25 +17,30 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) return nil } - protov6 := &tfprotov5.GetMetadataResponse{ + protov5 := &tfprotov5.GetMetadataResponse{ DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), Functions: make([]tfprotov5.FunctionMetadata, 0, len(fw.Functions)), Resources: make([]tfprotov5.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } for _, datasource := range fw.DataSources { - protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) + protov5.DataSources = append(protov5.DataSources, DataSourceMetadata(ctx, datasource)) + } + + for _, ephemeralResource := range fw.EphemeralResources { + protov5.EphemeralResources = append(protov5.EphemeralResources, EphemeralResourceMetadata(ctx, ephemeralResource)) } for _, function := range fw.Functions { - protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) + protov5.Functions = append(protov5.Functions, FunctionMetadata(ctx, function)) } for _, resource := range fw.Resources { - protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) + protov5.Resources = append(protov5.Resources, ResourceMetadata(ctx, resource)) } - return protov6 + return protov5 } diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go index 07001c939..a2d0c2bdc 100644 --- a/internal/toproto5/getmetadata_test.go +++ b/internal/toproto5/getmetadata_test.go @@ -45,6 +45,32 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, + "ephemeralresources": { + input: &fwserver.GetMetadataResponse{ + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, }, @@ -71,8 +97,9 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, }, }, "functions": { @@ -87,7 +114,8 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, Functions: []tfprotov5.FunctionMetadata{ { Name: "function1", @@ -111,8 +139,9 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource_1", @@ -131,9 +160,10 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, + DataSources: []tfprotov5.DataSourceMetadata{}, + EphemeralResources: []tfprotov5.EphemeralResourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -143,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.go b/internal/toproto5/getproviderschema.go index 1fec486ae..28b8906ff 100644 --- a/internal/toproto5/getproviderschema.go +++ b/internal/toproto5/getproviderschema.go @@ -18,11 +18,12 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov5 := &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), - ResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.ResourceSchemas)), - ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.EphemeralResourceSchemas)), + Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.ResourceSchemas)), + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } var err error @@ -83,5 +84,19 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for ephemeralResourceType, ephemeralResourceSchema := range fw.EphemeralResourceSchemas { + protov5.EphemeralResourceSchemas[ephemeralResourceType], err = Schema(ctx, ephemeralResourceSchema) + + if err != nil { + protov5.Diagnostics = append(protov5.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"" + ephemeralResourceType + "\" 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/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 1683b0e7e..ca293fa96 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -80,8 +81,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-computed": { @@ -110,8 +112,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-deprecated": { @@ -142,8 +145,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-optional": { @@ -172,8 +176,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-optional-computed": { @@ -204,8 +209,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-required": { @@ -234,8 +240,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-sensitive": { @@ -266,8 +273,41 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + 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": { @@ -296,8 +336,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-float32": { @@ -326,8 +367,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-float64": { @@ -356,8 +398,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-int32": { @@ -386,8 +429,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-int64": { @@ -416,8 +460,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-list-list-string": { @@ -453,8 +498,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-list-nested-attributes": { @@ -487,8 +533,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-list-object": { @@ -528,8 +575,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-list-string": { @@ -561,8 +609,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov5.Function{}, - ResourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, "data-source-attribute-type-map-nested-attributes": { @@ -595,16 +644,1109 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-map-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-number": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.NumberAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ObjectAttribute{ + Required: true, + AttributeTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-single-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SingleNestedAttribute{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting data source schema", + Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-dynamic": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.DynamicPseudoType, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-list": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.ListNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-set": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SetNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SingleNestedBlock{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-multiple-ephemeral-resources": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource_1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + "test_ephemeral_resource_2": 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_1": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + "test_ephemeral_resource_2": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-computed": { + 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", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-deprecated": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + DeprecationMessage: "deprecated", + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Deprecated: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Optional: 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", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-required": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-sensitive": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Sensitive: 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", + Sensitive: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + 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{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, + }, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + }, + }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-map-string": { + "ephemeral-resource-attribute-type-map-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapAttribute{ Required: true, ElementType: types.StringType, }, @@ -613,8 +1755,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -632,12 +1775,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-number": { + "ephemeral-resource-attribute-type-number": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.NumberAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.NumberAttribute{ Required: true, }, }, @@ -645,8 +1788,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -662,12 +1806,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-object": { + "ephemeral-resource-attribute-type-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ObjectAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ObjectAttribute{ Required: true, AttributeTypes: map[string]attr.Type{ "test_object_attribute": types.StringType, @@ -678,8 +1822,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -699,15 +1844,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-nested-attributes": { + "ephemeral-resource-attribute-type-set-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -719,26 +1864,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-object": { + "ephemeral-resource-attribute-type-set-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -751,8 +1897,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -774,12 +1921,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-set-string": { + "ephemeral-resource-attribute-type-set-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.SetType{ ElemType: types.StringType, @@ -790,8 +1937,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -811,12 +1959,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-set-string": { + "ephemeral-resource-attribute-type-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.StringType, }, @@ -825,8 +1973,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -844,14 +1993,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-single-nested-attributes": { + "ephemeral-resource-attribute-type-single-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SingleNestedAttribute{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SingleNestedAttribute{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -862,26 +2011,27 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": nil, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": nil, }, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Error converting data source schema", - Detail: "The schema for the data source \"test_data_source\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"test_ephemeral_resource\" 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\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-string": { + "ephemeral-resource-attribute-type-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -889,8 +2039,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -906,12 +2057,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-dynamic": { + "ephemeral-resource-attribute-type-dynamic": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.DynamicAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.DynamicAttribute{ Required: true, }, }, @@ -919,8 +2070,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ { @@ -936,15 +2088,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-list": { + "ephemeral-resource-block-list": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.ListNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.ListNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -955,8 +2107,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -980,15 +2133,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-set": { + "ephemeral-resource-block-set": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SetNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SetNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -999,8 +2152,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -1024,14 +2178,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-block-single": { + "ephemeral-resource-block-single": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SingleNestedBlock{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SingleNestedBlock{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1041,8 +2195,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ { @@ -1078,7 +2233,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction1": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1106,7 +2262,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { DeprecationMessage: "test deprecation message", @@ -1129,7 +2286,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Description: "test description", @@ -1156,7 +2314,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{ @@ -1187,7 +2346,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1209,7 +2369,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1232,7 +2393,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Functions: map[string]*tfprotov5.Function{ "testfunction": { Parameters: []*tfprotov5.FunctionParameter{}, @@ -1259,8 +2421,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1287,8 +2450,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1314,8 +2478,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1342,8 +2507,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1359,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{ @@ -1370,8 +2565,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1397,8 +2593,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1424,8 +2621,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1451,8 +2649,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1478,8 +2677,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1508,8 +2708,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1546,7 +2747,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1574,8 +2776,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1608,8 +2811,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1644,7 +2848,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1668,8 +2873,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1697,8 +2903,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1727,8 +2934,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1765,7 +2973,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1793,8 +3002,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1829,8 +3039,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1861,8 +3072,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1895,7 +3107,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -1918,8 +3131,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1945,8 +3159,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1978,8 +3193,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2019,8 +3235,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2058,8 +3275,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -2093,8 +3311,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2120,8 +3339,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2147,8 +3367,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2174,8 +3395,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2201,8 +3423,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2231,8 +3454,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2269,7 +3493,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2297,7 +3522,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2331,8 +3557,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2367,7 +3594,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2391,8 +3619,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2420,8 +3649,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2450,8 +3680,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2488,7 +3719,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2516,8 +3748,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2552,8 +3785,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2584,8 +3818,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2618,7 +3853,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -2641,8 +3877,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2677,8 +3914,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource_1": { Block: &tfprotov5.SchemaBlock{ @@ -2718,8 +3956,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2749,8 +3988,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2780,8 +4020,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2811,8 +4052,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2842,8 +4084,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2873,8 +4116,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2891,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{ @@ -2904,8 +4181,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2934,8 +4212,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2964,8 +4243,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -2994,8 +4274,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3024,8 +4305,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3057,8 +4339,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3098,7 +4381,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3130,8 +4414,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3167,8 +4452,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3206,7 +4492,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3234,8 +4521,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3266,8 +4554,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3299,8 +4588,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3340,7 +4630,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3372,8 +4663,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3411,8 +4703,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3446,8 +4739,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3483,7 +4777,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, @@ -3510,8 +4805,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3540,8 +4836,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3576,8 +4873,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3620,8 +4918,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3662,8 +4961,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{ @@ -3696,8 +4996,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, - Functions: map[string]*tfprotov5.Function{}, + 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{}, @@ -3709,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/importedresource_test.go b/internal/toproto5/importedresource_test.go index a1e9e4146..58a1e860b 100644 --- a/internal/toproto5/importedresource_test.go +++ b/internal/toproto5/importedresource_test.go @@ -199,8 +199,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_test.go b/internal/toproto5/moveresourcestate_test.go index 76712ee64..36075c0bf 100644 --- a/internal/toproto5/moveresourcestate_test.go +++ b/internal/toproto5/moveresourcestate_test.go @@ -155,8 +155,6 @@ func TestMoveResourceStateResponse(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/openephemeralresource.go b/internal/toproto5/openephemeralresource.go new file mode 100644 index 000000000..2ea4fac50 --- /dev/null +++ b/internal/toproto5/openephemeralresource.go @@ -0,0 +1,37 @@ +// 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" +) + +// OpenEphemeralResourceResponse returns the *tfprotov5.OpenEphemeralResourceResponse +// equivalent of a *fwserver.OpenEphemeralResourceResponse. +func OpenEphemeralResourceResponse(ctx context.Context, fw *fwserver.OpenEphemeralResourceResponse) *tfprotov5.OpenEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.OpenEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + Deferred: EphemeralResourceDeferred(fw.Deferred), + } + + result, diags := EphemeralResultData(ctx, fw.Result) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Result = result + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + + return proto5 +} diff --git a/internal/toproto5/openephemeralresource_test.go b/internal/toproto5/openephemeralresource_test.go new file mode 100644 index 000000000..fe6cf04c4 --- /dev/null +++ b/internal/toproto5/openephemeralresource_test.go @@ -0,0 +1,212 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + "time" + + "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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/tfsdk" +) + +func TestOpenEphemeralResourceResponse(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) + } + + testDeferral := &ephemeral.Deferred{ + Reason: ephemeral.DeferredReasonAbsentPrereq, + } + + testProto5Deferred := &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonAbsentPrereq, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEphemeralResult := &tfsdk.EphemeralResultData{ + Raw: testProto5Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + } + + testEphemeralResultInvalid := &tfsdk.EphemeralResultData{ + Raw: testProto5Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.OpenEphemeralResourceResponse + expected *tfprotov5.OpenEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.OpenEphemeralResourceResponse{}, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + "diagnostics-invalid-result": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + Result: testEphemeralResultInvalid, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + 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 Ephemeral Result Data", + Detail: "An unexpected error was encountered when converting the ephemeral result data 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", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "result": { + input: &fwserver.OpenEphemeralResourceResponse{ + Result: testEphemeralResult, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Result: &testProto5DynamicValue, + }, + }, + "private-empty": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + "deferral": { + input: &fwserver.OpenEphemeralResourceResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov5.OpenEphemeralResourceResponse{ + Deferred: testProto5Deferred, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.OpenEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index d4ed9e674..05924b5b4 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -200,8 +200,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_test.go b/internal/toproto5/readresource_test.go index 582e7090a..2e9c549d3 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -187,8 +187,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.go b/internal/toproto5/renewephemeralresource.go new file mode 100644 index 000000000..5947c9dd1 --- /dev/null +++ b/internal/toproto5/renewephemeralresource.go @@ -0,0 +1,31 @@ +// 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" +) + +// RenewEphemeralResourceResponse returns the *tfprotov5.RenewEphemeralResourceResponse +// equivalent of a *fwserver.RenewEphemeralResourceResponse. +func RenewEphemeralResourceResponse(ctx context.Context, fw *fwserver.RenewEphemeralResourceResponse) *tfprotov5.RenewEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + } + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.Private = newPrivate + + return proto5 +} diff --git a/internal/toproto5/renewephemeralresource_test.go b/internal/toproto5/renewephemeralresource_test.go new file mode 100644 index 000000000..a1c55bf03 --- /dev/null +++ b/internal/toproto5/renewephemeralresource_test.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" +) + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testCases := map[string]struct { + input *fwserver.RenewEphemeralResourceResponse + expected *tfprotov5.RenewEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.RenewEphemeralResourceResponse{}, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 5, 10, 32, 0, time.UTC), + }, + }, + "private-empty": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov5.RenewEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.RenewEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected 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.go b/internal/toproto5/server_capabilities.go index fd968906d..2ed77acbe 100644 --- a/internal/toproto5/server_capabilities.go +++ b/internal/toproto5/server_capabilities.go @@ -19,6 +19,7 @@ func ServerCapabilities(ctx context.Context, fw *fwserver.ServerCapabilities) *t return &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: fw.GetProviderSchemaOptional, + MoveResourceState: fw.MoveResourceState, PlanDestroy: fw.PlanDestroy, } } diff --git a/internal/toproto5/server_capabilities_test.go b/internal/toproto5/server_capabilities_test.go index 772909e56..bf78fd7d4 100644 --- a/internal/toproto5/server_capabilities_test.go +++ b/internal/toproto5/server_capabilities_test.go @@ -32,6 +32,14 @@ func TestServerCapabilities(t *testing.T) { GetProviderSchemaOptional: true, }, }, + "MoveResourceState": { + fw: &fwserver.ServerCapabilities{ + MoveResourceState: true, + }, + expected: &tfprotov5.ServerCapabilities{ + MoveResourceState: true, + }, + }, "PlanDestroy": { fw: &fwserver.ServerCapabilities{ PlanDestroy: true, @@ -43,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.go b/internal/toproto5/validateephemeralresourceconfig.go new file mode 100644 index 000000000..fcd19ac99 --- /dev/null +++ b/internal/toproto5/validateephemeralresourceconfig.go @@ -0,0 +1,25 @@ +// 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" +) + +// ValidateEphemeralResourceConfigResponse returns the *tfprotov5.ValidateEphemeralResourceConfigResponse +// equivalent of a *fwserver.ValidateEphemeralResourceConfigResponse. +func ValidateEphemeralResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateEphemeralResourceConfigResponse) *tfprotov5.ValidateEphemeralResourceConfigResponse { + if fw == nil { + return nil + } + + proto5 := &tfprotov5.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto5 +} diff --git a/internal/toproto5/validateephemeralresourceconfig_test.go b/internal/toproto5/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..a3c4c7dd6 --- /dev/null +++ b/internal/toproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,67 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateEphemeralResourceConfigResponse + expected *tfprotov5.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{}, + expected: &tfprotov5.ValidateEphemeralResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov5.ValidateEphemeralResourceConfigResponse{ + 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", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ValidateEphemeralResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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_test.go b/internal/toproto6/applyresourcechange_test.go index 9a4d1f886..5084841d1 100644 --- a/internal/toproto6/applyresourcechange_test.go +++ b/internal/toproto6/applyresourcechange_test.go @@ -157,8 +157,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.go b/internal/toproto6/closeephemeralresource.go new file mode 100644 index 000000000..46810b9d7 --- /dev/null +++ b/internal/toproto6/closeephemeralresource.go @@ -0,0 +1,25 @@ +// 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" +) + +// CloseEphemeralResourceResponse returns the *tfprotov6.CloseEphemeralResourceResponse +// equivalent of a *fwserver.CloseEphemeralResourceResponse. +func CloseEphemeralResourceResponse(ctx context.Context, fw *fwserver.CloseEphemeralResourceResponse) *tfprotov6.CloseEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.CloseEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/closeephemeralresource_test.go b/internal/toproto6/closeephemeralresource_test.go new file mode 100644 index 000000000..05bd2cd1b --- /dev/null +++ b/internal/toproto6/closeephemeralresource_test.go @@ -0,0 +1,67 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestCloseEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CloseEphemeralResourceResponse + expected *tfprotov6.CloseEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.CloseEphemeralResourceResponse{}, + expected: &tfprotov6.CloseEphemeralResourceResponse{}, + }, + "diagnostics": { + input: &fwserver.CloseEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.CloseEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.CloseEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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/deferred.go b/internal/toproto6/deferred.go index 10afa8fdc..fab64fe05 100644 --- a/internal/toproto6/deferred.go +++ b/internal/toproto6/deferred.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -27,3 +28,12 @@ func ResourceDeferred(fw *resource.Deferred) *tfprotov6.Deferred { Reason: tfprotov6.DeferredReason(fw.Reason), } } + +func EphemeralResourceDeferred(fw *ephemeral.Deferred) *tfprotov6.Deferred { + if fw == nil { + return nil + } + return &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReason(fw.Reason), + } +} 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.go b/internal/toproto6/ephemeral_result_data.go new file mode 100644 index 000000000..14144adb2 --- /dev/null +++ b/internal/toproto6/ephemeral_result_data.go @@ -0,0 +1,28 @@ +// 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" +) + +// EphemeralResultData returns the *tfprotov6.DynamicValue for a *tfsdk.EphemeralResultData. +func EphemeralResultData(ctx context.Context, fw *tfsdk.EphemeralResultData) (*tfprotov6.DynamicValue, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + data := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralResultData, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + return DynamicValue(ctx, data) +} diff --git a/internal/toproto6/ephemeral_result_data_test.go b/internal/toproto6/ephemeral_result_data_test.go new file mode 100644 index 000000000..14705f5fe --- /dev/null +++ b/internal/toproto6/ephemeral_result_data_test.go @@ -0,0 +1,107 @@ +// 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 TestEphemeralResultData(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) + } + + testEphemeralResultData := &tfsdk.EphemeralResultData{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testEphemeralResultDataInvalid := &tfsdk.EphemeralResultData{ + 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.EphemeralResultData + expected *tfprotov6.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testEphemeralResultDataInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Ephemeral Result Data", + "An unexpected error was encountered when converting the ephemeral result data 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: testEphemeralResultData, + expected: &testProto6DynamicValue, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto6.EphemeralResultData(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/ephemeralresourcemetadata.go b/internal/toproto6/ephemeralresourcemetadata.go new file mode 100644 index 000000000..56fab9951 --- /dev/null +++ b/internal/toproto6/ephemeralresourcemetadata.go @@ -0,0 +1,19 @@ +// 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" +) + +// EphemeralResourceMetadata returns the tfprotov6.EphemeralResourceMetadata for a +// fwserver.EphemeralResourceMetadata. +func EphemeralResourceMetadata(ctx context.Context, fw fwserver.EphemeralResourceMetadata) tfprotov6.EphemeralResourceMetadata { + return tfprotov6.EphemeralResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto6/ephemeralresourcemetadata_test.go b/internal/toproto6/ephemeralresourcemetadata_test.go new file mode 100644 index 000000000..d14a22605 --- /dev/null +++ b/internal/toproto6/ephemeralresourcemetadata_test.go @@ -0,0 +1,44 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestEphemeralResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.EphemeralResourceMetadata + expected tfprotov6.EphemeralResourceMetadata + }{ + "TypeName": { + fw: fwserver.EphemeralResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov6.EphemeralResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.EphemeralResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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.go b/internal/toproto6/getmetadata.go index 0924f3c9f..314072392 100644 --- a/internal/toproto6/getmetadata.go +++ b/internal/toproto6/getmetadata.go @@ -20,6 +20,7 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6 := &tfprotov6.GetMetadataResponse{ DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0, len(fw.EphemeralResources)), Functions: make([]tfprotov6.FunctionMetadata, 0, len(fw.Functions)), Resources: make([]tfprotov6.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), @@ -29,6 +30,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) } + for _, ephemeralResource := range fw.EphemeralResources { + protov6.EphemeralResources = append(protov6.EphemeralResources, EphemeralResourceMetadata(ctx, ephemeralResource)) + } + for _, function := range fw.Functions { protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) } diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go index 5c0590500..fe02ebf1c 100644 --- a/internal/toproto6/getmetadata_test.go +++ b/internal/toproto6/getmetadata_test.go @@ -45,8 +45,9 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, }, }, "diagnostics": { @@ -71,6 +72,32 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, + "ephemeralresources": { + input: &fwserver.GetMetadataResponse{ + EphemeralResources: []fwserver.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ + { + TypeName: "test_ephemeral_resource_1", + }, + { + TypeName: "test_ephemeral_resource_2", + }, + }, Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, }, @@ -87,7 +114,8 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, Functions: []tfprotov6.FunctionMetadata{ { Name: "function1", @@ -111,8 +139,9 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource_1", @@ -131,9 +160,10 @@ func TestGetMetadataResponse(t *testing.T) { }, }, expected: &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, - Functions: []tfprotov6.FunctionMetadata{}, - Resources: []tfprotov6.ResourceMetadata{}, + DataSources: []tfprotov6.DataSourceMetadata{}, + EphemeralResources: []tfprotov6.EphemeralResourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -143,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.go b/internal/toproto6/getproviderschema.go index ee221abbf..d88a5381e 100644 --- a/internal/toproto6/getproviderschema.go +++ b/internal/toproto6/getproviderschema.go @@ -18,11 +18,12 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov6 := &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), - ResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.ResourceSchemas)), - ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + EphemeralResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.EphemeralResourceSchemas)), + Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.ResourceSchemas)), + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } var err error @@ -83,5 +84,19 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for ephemeralResourceType, ephemeralResourceSchema := range fw.EphemeralResourceSchemas { + protov6.EphemeralResourceSchemas[ephemeralResourceType], err = Schema(ctx, ephemeralResourceSchema) + + if err != nil { + protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error converting ephemeral resource schema", + Detail: "The schema for the ephemeral resource \"" + ephemeralResourceType + "\" 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/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index cba045d18..1b3d1da19 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + ephemeralschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -80,8 +81,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-computed": { @@ -110,8 +112,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-deprecated": { @@ -142,8 +145,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-optional": { @@ -172,8 +176,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-optional-computed": { @@ -204,8 +209,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-required": { @@ -234,8 +240,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-sensitive": { @@ -266,8 +273,41 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + 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": { @@ -296,8 +336,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-float64": { @@ -326,8 +367,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-int32": { @@ -356,8 +398,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-int64": { @@ -386,8 +429,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-list-list-string": { @@ -423,8 +467,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-list-nested-attributes": { @@ -469,8 +514,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-list-object": { @@ -510,8 +556,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-list-string": { @@ -543,8 +590,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, - Functions: map[string]*tfprotov6.Function{}, - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, "data-source-attribute-type-map-nested-attributes": { @@ -567,8 +615,1117 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeMap, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-map-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-number": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.NumberAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ObjectAttribute{ + Required: true, + AttributeTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetNestedAttribute{ + NestedObject: datasourceschema.NestedAttributeObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSet, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-set-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SetAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-single-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.SingleNestedAttribute{ + Attributes: map[string]datasourceschema.Attribute{ + "test_nested_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-dynamic": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.DynamicAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.DynamicPseudoType, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-list": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.ListNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-set": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SetNestedBlock{ + NestedObject: datasourceschema.NestedBlockObject{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-block-single": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Blocks: map[string]datasourceschema.Block{ + "test_block": datasourceschema.SingleNestedBlock{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + TypeName: "test_block", + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-multiple-ephemeral-resources": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource_1": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + "test_ephemeral_resource_2": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource_1": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + "test_ephemeral_resource_2": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-computed": { + 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: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-deprecated": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + DeprecationMessage: "deprecated", + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Deprecated: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-optional-computed": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Optional: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-required": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-sensitive": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + Sensitive: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + Sensitive: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-bool": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-float64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Float64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int32": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int32Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-int64": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int64Attribute{ + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_nested_attribute", + Type: tftypes.String, + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ + Required: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Required: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "ephemeral-resource-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -593,12 +1750,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-map-string": { + "ephemeral-resource-attribute-type-map-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.MapAttribute{ Required: true, ElementType: types.StringType, }, @@ -607,8 +1764,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -626,12 +1784,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-number": { + "ephemeral-resource-attribute-type-number": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.NumberAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.NumberAttribute{ Required: true, }, }, @@ -639,8 +1797,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -656,12 +1815,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-object": { + "ephemeral-resource-attribute-type-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ObjectAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ObjectAttribute{ Required: true, AttributeTypes: map[string]attr.Type{ "test_object_attribute": types.StringType, @@ -672,8 +1831,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -693,15 +1853,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-nested-attributes": { + "ephemeral-resource-attribute-type-set-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetNestedAttribute{ - NestedObject: datasourceschema.NestedAttributeObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -713,8 +1873,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -739,12 +1900,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-object": { + "ephemeral-resource-attribute-type-set-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -757,8 +1918,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -780,12 +1942,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-set-string": { + "ephemeral-resource-attribute-type-set-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.SetType{ ElemType: types.StringType, @@ -796,8 +1958,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -817,12 +1980,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-set-string": { + "ephemeral-resource-attribute-type-set-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SetAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SetAttribute{ Required: true, ElementType: types.StringType, }, @@ -831,8 +1994,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -850,14 +2014,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-single-nested-attributes": { + "ephemeral-resource-attribute-type-single-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.SingleNestedAttribute{ - Attributes: map[string]datasourceschema.Attribute{ - "test_nested_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.SingleNestedAttribute{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -868,8 +2032,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -894,12 +2059,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-string": { + "ephemeral-resource-attribute-type-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -907,8 +2072,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -924,12 +2090,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-dynamic": { + "ephemeral-resource-attribute-type-dynamic": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.DynamicAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.DynamicAttribute{ Required: true, }, }, @@ -937,8 +2103,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ { @@ -954,15 +2121,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-list": { + "ephemeral-resource-block-list": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.ListNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.ListNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -973,8 +2140,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -998,15 +2166,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-set": { + "ephemeral-resource-block-set": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SetNestedBlock{ - NestedObject: datasourceschema.NestedBlockObject{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SetNestedBlock{ + NestedObject: ephemeralschema.NestedBlockObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1017,8 +2185,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -1042,14 +2211,14 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-block-single": { + "ephemeral-resource-block-single": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Blocks: map[string]datasourceschema.Block{ - "test_block": datasourceschema.SingleNestedBlock{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.StringAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Blocks: map[string]ephemeralschema.Block{ + "test_block": ephemeralschema.SingleNestedBlock{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -1059,8 +2228,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{ - "test_data_source": { + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": { Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ { @@ -1096,7 +2266,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction1": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1124,7 +2295,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { DeprecationMessage: "test deprecation message", @@ -1147,7 +2319,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Description: "test description", @@ -1174,7 +2347,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{ @@ -1205,7 +2379,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1227,7 +2402,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1250,7 +2426,8 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, Functions: map[string]*tfprotov6.Function{ "testfunction": { Parameters: []*tfprotov6.FunctionParameter{}, @@ -1277,8 +2454,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1305,8 +2483,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1332,8 +2511,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1360,8 +2540,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1377,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{ @@ -1388,8 +2598,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1415,8 +2626,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1442,8 +2654,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1469,8 +2682,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1499,8 +2713,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1537,8 +2752,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1578,8 +2794,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1612,8 +2829,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1648,8 +2866,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1685,8 +2904,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1714,8 +2934,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1744,8 +2965,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1782,8 +3004,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1823,8 +3046,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1859,8 +3083,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1891,8 +3116,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1925,8 +3151,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1961,8 +3188,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1988,8 +3216,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2021,8 +3250,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2062,8 +3292,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2101,8 +3332,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -2136,8 +3368,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2163,8 +3396,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2190,8 +3424,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2217,8 +3452,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2244,8 +3480,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2274,8 +3511,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2312,8 +3550,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2353,8 +3592,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2387,8 +3627,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2423,8 +3664,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2460,8 +3702,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2489,8 +3732,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2519,8 +3763,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2557,8 +3802,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2598,8 +3844,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2634,8 +3881,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2666,8 +3914,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2700,8 +3949,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2736,8 +3986,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2772,8 +4023,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource_1": { Block: &tfprotov6.SchemaBlock{ @@ -2813,8 +4065,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2844,8 +4097,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2875,8 +4129,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2906,8 +4161,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2937,8 +4193,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2968,8 +4225,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -2986,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{ @@ -2999,8 +4290,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3029,8 +4321,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3059,8 +4352,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3089,8 +4383,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3122,8 +4417,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3163,8 +4459,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3207,8 +4504,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3244,8 +4542,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3283,8 +4582,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3323,8 +4623,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3355,8 +4656,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3388,8 +4690,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3429,8 +4732,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3473,8 +4777,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3512,8 +4817,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3547,8 +4853,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3584,8 +4891,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3623,8 +4931,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3653,8 +4962,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3689,8 +4999,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3733,8 +5044,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3775,8 +5087,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{ @@ -3809,8 +5122,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, expected: &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, - Functions: map[string]*tfprotov6.Function{}, + 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{}, @@ -3822,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/importedresource_test.go b/internal/toproto6/importedresource_test.go index ce89d34f9..248d0d437 100644 --- a/internal/toproto6/importedresource_test.go +++ b/internal/toproto6/importedresource_test.go @@ -199,8 +199,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_test.go b/internal/toproto6/moveresourcestate_test.go index 8a40223ee..d5281c970 100644 --- a/internal/toproto6/moveresourcestate_test.go +++ b/internal/toproto6/moveresourcestate_test.go @@ -155,8 +155,6 @@ func TestMoveResourceStateResponse(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/openephemeralresource.go b/internal/toproto6/openephemeralresource.go new file mode 100644 index 000000000..7da9c35f6 --- /dev/null +++ b/internal/toproto6/openephemeralresource.go @@ -0,0 +1,37 @@ +// 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" +) + +// OpenEphemeralResourceResponse returns the *tfprotov6.OpenEphemeralResourceResponse +// equivalent of a *fwserver.OpenEphemeralResourceResponse. +func OpenEphemeralResourceResponse(ctx context.Context, fw *fwserver.OpenEphemeralResourceResponse) *tfprotov6.OpenEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.OpenEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + Deferred: EphemeralResourceDeferred(fw.Deferred), + } + + result, diags := EphemeralResultData(ctx, fw.Result) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Result = result + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + + return proto6 +} diff --git a/internal/toproto6/openephemeralresource_test.go b/internal/toproto6/openephemeralresource_test.go new file mode 100644 index 000000000..4343c3e26 --- /dev/null +++ b/internal/toproto6/openephemeralresource_test.go @@ -0,0 +1,212 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + "time" + + "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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "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/tfsdk" +) + +func TestOpenEphemeralResourceResponse(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) + } + + testDeferral := &ephemeral.Deferred{ + Reason: ephemeral.DeferredReasonAbsentPrereq, + } + + testProto6Deferred := &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testEphemeralResult := &tfsdk.EphemeralResultData{ + Raw: testProto6Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.StringAttribute{ + Required: true, + }, + }, + }, + } + + testEphemeralResultInvalid := &tfsdk.EphemeralResultData{ + Raw: testProto6Value, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + } + + testCases := map[string]struct { + input *fwserver.OpenEphemeralResourceResponse + expected *tfprotov6.OpenEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.OpenEphemeralResourceResponse{}, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + "diagnostics-invalid-result": { + input: &fwserver.OpenEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + Result: testEphemeralResultInvalid, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + 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 Ephemeral Result Data", + Detail: "An unexpected error was encountered when converting the ephemeral result data 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", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "state": { + input: &fwserver.OpenEphemeralResourceResponse{ + Result: testEphemeralResult, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Result: &testProto6DynamicValue, + }, + }, + "private-empty": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.OpenEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + "deferral": { + input: &fwserver.OpenEphemeralResourceResponse{ + Deferred: testDeferral, + }, + expected: &tfprotov6.OpenEphemeralResourceResponse{ + Deferred: testProto6Deferred, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.OpenEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index ad8376e16..376ec1f9b 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -200,8 +200,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_test.go b/internal/toproto6/readresource_test.go index 46d928c31..18842699e 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -187,8 +187,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.go b/internal/toproto6/renewephemeralresource.go new file mode 100644 index 000000000..4afcb9962 --- /dev/null +++ b/internal/toproto6/renewephemeralresource.go @@ -0,0 +1,31 @@ +// 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" +) + +// RenewEphemeralResourceResponse returns the *tfprotov6.RenewEphemeralResourceResponse +// equivalent of a *fwserver.RenewEphemeralResourceResponse. +func RenewEphemeralResourceResponse(ctx context.Context, fw *fwserver.RenewEphemeralResourceResponse) *tfprotov6.RenewEphemeralResourceResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.RenewEphemeralResourceResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + RenewAt: fw.RenewAt, + } + + newPrivate, diags := fw.Private.Bytes(ctx) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.Private = newPrivate + + return proto6 +} diff --git a/internal/toproto6/renewephemeralresource_test.go b/internal/toproto6/renewephemeralresource_test.go new file mode 100644 index 000000000..1491f3bc4 --- /dev/null +++ b/internal/toproto6/renewephemeralresource_test.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" +) + +func TestRenewEphemeralResourceResponse(t *testing.T) { + t.Parallel() + + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }) + + testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) + + testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) + + testCases := map[string]struct { + input *fwserver.RenewEphemeralResourceResponse + expected *tfprotov6.RenewEphemeralResourceResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.RenewEphemeralResourceResponse{}, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + // Time zero + RenewAt: *new(time.Time), + }, + }, + "diagnostics": { + input: &fwserver.RenewEphemeralResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + 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", + }, + }, + }, + }, + "renew-at": { + input: &fwserver.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + RenewAt: time.Date(2024, 8, 29, 6, 10, 32, 0, time.UTC), + }, + }, + "private-empty": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{}, + Provider: testEmptyProviderData, + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Private: nil, + }, + }, + "private": { + input: &fwserver.RenewEphemeralResourceResponse{ + Private: &privatestate.Data{ + Framework: map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`)}, + Provider: testProviderData, + }, + }, + expected: &tfprotov6.RenewEphemeralResourceResponse{ + Private: privatestate.MustMarshalToJson(map[string][]byte{ + ".frameworkKey": []byte(`{"fKeyOne": {"k0": "zero", "k1": 1}}`), + "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), + }), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.RenewEphemeralResourceResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected 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.go b/internal/toproto6/server_capabilities.go index ef46cbf16..26c24f7c6 100644 --- a/internal/toproto6/server_capabilities.go +++ b/internal/toproto6/server_capabilities.go @@ -19,6 +19,7 @@ func ServerCapabilities(ctx context.Context, fw *fwserver.ServerCapabilities) *t return &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: fw.GetProviderSchemaOptional, + MoveResourceState: fw.MoveResourceState, PlanDestroy: fw.PlanDestroy, } } diff --git a/internal/toproto6/server_capabilities_test.go b/internal/toproto6/server_capabilities_test.go index f3a91ac03..557db6c32 100644 --- a/internal/toproto6/server_capabilities_test.go +++ b/internal/toproto6/server_capabilities_test.go @@ -32,6 +32,14 @@ func TestServerCapabilities(t *testing.T) { GetProviderSchemaOptional: true, }, }, + "MoveResourceState": { + fw: &fwserver.ServerCapabilities{ + MoveResourceState: true, + }, + expected: &tfprotov6.ServerCapabilities{ + MoveResourceState: true, + }, + }, "PlanDestroy": { fw: &fwserver.ServerCapabilities{ PlanDestroy: true, @@ -43,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.go b/internal/toproto6/validateephemeralresourceconfig.go new file mode 100644 index 000000000..5237b35dc --- /dev/null +++ b/internal/toproto6/validateephemeralresourceconfig.go @@ -0,0 +1,25 @@ +// 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" +) + +// ValidateEphemeralResourceConfigResponse returns the *tfprotov6.ValidateEphemeralResourceConfigResponse +// equivalent of a *fwserver.ValidateEphemeralResourceConfigResponse. +func ValidateEphemeralResourceConfigResponse(ctx context.Context, fw *fwserver.ValidateEphemeralResourceConfigResponse) *tfprotov6.ValidateEphemeralResourceConfigResponse { + if fw == nil { + return nil + } + + proto6 := &tfprotov6.ValidateEphemeralResourceConfigResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + return proto6 +} diff --git a/internal/toproto6/validateephemeralresourceconfig_test.go b/internal/toproto6/validateephemeralresourceconfig_test.go new file mode 100644 index 000000000..3b41fdf1d --- /dev/null +++ b/internal/toproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,67 @@ +// 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/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestValidateEphemeralResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.ValidateEphemeralResourceConfigResponse + expected *tfprotov6.ValidateEphemeralResourceConfigResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{}, + expected: &tfprotov6.ValidateEphemeralResourceConfigResponse{}, + }, + "diagnostics": { + input: &fwserver.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + }, + expected: &tfprotov6.ValidateEphemeralResourceConfigResponse{ + 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", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ValidateEphemeralResourceConfigResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} 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/configure.go b/provider/configure.go index 9b6678bf7..59e9ead44 100644 --- a/provider/configure.go +++ b/provider/configure.go @@ -62,6 +62,11 @@ type ConfigureResponse struct { // that implements the Configure method. ResourceData any + // EphemeralResourceData is provider-defined data, clients, etc. that is + // passed to [ephemeral.ConfigureRequest.ProviderData] for each + // EphemeralResource type that implements the Configure method. + EphemeralResourceData any + // Deferred indicates that Terraform should automatically defer // all resources and data sources for this provider. // diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 6d96a516a..374296341 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,9 @@ 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 +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go index 617225b54..f679fc1a5 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,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index 8a4478655..ac2b79b0a 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,9 @@ 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 +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go index 763da9ac7..8a73c9225 100644 --- a/provider/metaschema/float64_attribute_test.go +++ b/provider/metaschema/float64_attribute_test.go @@ -9,12 +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) { @@ -53,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() @@ -95,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() @@ -130,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() @@ -164,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() @@ -198,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() @@ -223,17 +215,15 @@ func TestFloat64AttributeGetType(t *testing.T) { attribute: metaschema.Float64Attribute{}, expected: types.Float64Type, }, - // "custom-type": { - // attribute: metaschema.Float64Attribute{ - // CustomType: testtypes.Float64Type{}, - // }, - // expected: testtypes.Float64Type{}, - // }, + "custom-type": { + attribute: metaschema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,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() @@ -294,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() @@ -328,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() @@ -356,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() @@ -369,3 +351,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index 8751d574e..aeccd7030 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,9 @@ 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 +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go index 55a3c96f0..69439d021 100644 --- a/provider/metaschema/int64_attribute_test.go +++ b/provider/metaschema/int64_attribute_test.go @@ -9,12 +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) { @@ -53,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() @@ -95,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() @@ -130,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() @@ -164,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() @@ -198,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() @@ -223,17 +215,15 @@ func TestInt64AttributeGetType(t *testing.T) { attribute: metaschema.Int64Attribute{}, expected: types.Int64Type, }, - // "custom-type": { - // attribute: metaschema.Int64Attribute{ - // CustomType: testtypes.Int64Type{}, - // }, - // expected: testtypes.Int64Type{}, - // }, + "custom-type": { + attribute: metaschema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -260,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() @@ -294,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() @@ -328,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() @@ -356,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() @@ -369,3 +351,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index a3ff30e65..187d9c47c 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,12 @@ 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 +} + // 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 0677b66e6..93f213ce0 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() @@ -232,17 +223,15 @@ func TestListAttributeGetType(t *testing.T) { attribute: metaschema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: metaschema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: metaschema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index a6b4f875a..0fa1b8221 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,9 @@ 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 +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go index 56ca2e1f8..1c3576a30 100644 --- a/provider/metaschema/list_nested_attribute_test.go +++ b/provider/metaschema/list_nested_attribute_test.go @@ -9,12 +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) { @@ -81,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() @@ -129,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() @@ -220,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() @@ -260,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() @@ -300,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() @@ -338,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() @@ -375,17 +365,15 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: metaschema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: metaschema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,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() @@ -458,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() @@ -498,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() @@ -532,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() @@ -545,3 +525,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index d75cec982..9103231ff 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. @@ -19,7 +20,7 @@ var ( _ fwschema.AttributeWithValidateImplementation = MapAttribute{} ) -// MapAttribute represents a schema attribute that is a list with a single +// MapAttribute represents a schema attribute that is a map with a single // element type. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The ElementType field // must be set. @@ -28,7 +29,7 @@ var ( // require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a list or directly via curly brace syntax. +// return a map or directly via curly brace syntax. // // # map of strings // example_attribute = { @@ -137,6 +138,12 @@ 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 +} + // 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 9f4f5f32f..7d137ef1d 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() @@ -232,17 +223,15 @@ func TestMapAttributeGetType(t *testing.T) { attribute: metaschema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: metaschema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: metaschema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index a8809b7a6..587c56c0a 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. @@ -18,7 +19,7 @@ var ( _ NestedAttribute = MapNestedAttribute{} ) -// MapNestedAttribute represents an attribute that is a set of objects where +// MapNestedAttribute represents an attribute that is a map of objects where // the object attributes can be fully defined, including further nested // attributes. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The NestedObject field @@ -28,7 +29,7 @@ var ( // not require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a set of objects or directly via curly brace syntax. +// return a map of objects or directly via curly brace syntax. // // # map of objects // example_attribute = { @@ -123,7 +124,7 @@ func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeMap. func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeMap } @@ -159,3 +160,9 @@ 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 +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go index a014c6429..99c5ae558 100644 --- a/provider/metaschema/map_nested_attribute_test.go +++ b/provider/metaschema/map_nested_attribute_test.go @@ -9,12 +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) { @@ -81,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() @@ -129,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() @@ -220,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() @@ -260,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() @@ -300,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() @@ -338,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() @@ -375,17 +365,15 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: metaschema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: metaschema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,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() @@ -458,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() @@ -498,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() @@ -532,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() @@ -545,3 +525,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/nested_attribute_object_test.go b/provider/metaschema/nested_attribute_object_test.go index afb466785..3ea0c0ac1 100644 --- a/provider/metaschema/nested_attribute_object_test.go +++ b/provider/metaschema/nested_attribute_object_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "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" @@ -78,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() @@ -143,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() @@ -183,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() @@ -216,17 +211,15 @@ func TestNestedAttributeObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: metaschema.NestedAttributeObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: metaschema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..511e7000a 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,9 @@ 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 +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go index 819f2d2a1..676db8e1b 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,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index aa4c67be9..aabe40d4c 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,12 @@ 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 +} + // 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 93acca227..6b3bb4479 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() @@ -238,17 +229,15 @@ func TestObjectAttributeGetType(t *testing.T) { attribute: metaschema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, }, - // "custom-type": { - // attribute: metaschema.ObjectAttribute{ - // CustomType: testtypes.ObjectType{}, - // }, - // expected: testtypes.ObjectType{}, - // }, + "custom-type": { + attribute: metaschema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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() 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..f7d3e4112 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,12 @@ 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 +} + // 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 9e0bbb18e..975b3b719 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() @@ -232,17 +223,15 @@ func TestSetAttributeGetType(t *testing.T) { attribute: metaschema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: metaschema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: metaschema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index 8bbcf1259..a3c6fbf9e 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. @@ -118,7 +119,7 @@ func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSet. func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSet } @@ -154,3 +155,9 @@ 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 +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go index 8ac6ed87a..225a936f9 100644 --- a/provider/metaschema/set_nested_attribute_test.go +++ b/provider/metaschema/set_nested_attribute_test.go @@ -9,12 +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) { @@ -81,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() @@ -129,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() @@ -220,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() @@ -260,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() @@ -300,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() @@ -338,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() @@ -375,17 +365,15 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: metaschema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: metaschema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -418,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() @@ -458,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() @@ -498,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() @@ -532,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() @@ -545,3 +525,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index de4dc07a4..160fb1c80 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. @@ -132,7 +133,7 @@ func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject } } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSingle. func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSingle } @@ -174,3 +175,9 @@ 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 +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go index a8ce22e63..d590cbba8 100644 --- a/provider/metaschema/single_nested_attribute_test.go +++ b/provider/metaschema/single_nested_attribute_test.go @@ -9,12 +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) { @@ -79,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() @@ -170,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() @@ -202,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() @@ -240,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() @@ -278,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() @@ -314,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() @@ -347,17 +337,15 @@ func TestSingleNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: metaschema.SingleNestedAttribute{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + attribute: metaschema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -388,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() @@ -426,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() @@ -464,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() @@ -496,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() @@ -509,3 +489,29 @@ 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) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 3a14d0721..fe25c5014 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,9 @@ 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 +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go index 544fa268a..c360462a1 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,29 @@ 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) + } + }) + } +} diff --git a/provider/provider.go b/provider/provider.go index ff0d18e81..61dfd39fe 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -85,6 +86,21 @@ type ProviderWithFunctions interface { Functions(context.Context) []func() function.Function } +// ProviderWithEphemeralResources is an interface type that extends Provider to +// include ephemeral resources for usage in practitioner configurations. +// +// Ephemeral resources are supported in Terraform version 1.10 and later. +type ProviderWithEphemeralResources interface { + Provider + + // EphemeralResources returns a slice of functions to instantiate each EphemeralResource + // implementation. + // + // The ephemeral resource type name is determined by the EphemeralResource implementing + // the Metadata method. All ephemeral resources must have unique names. + EphemeralResources(context.Context) []func() ephemeral.EphemeralResource +} + // ProviderWithMetaSchema is a provider with a provider meta schema, which // is configured by practitioners via the provider_meta configuration block // and the configuration data is included with certain data source and resource diff --git a/provider/schema/attribute.go b/provider/schema/attribute.go index 4a0feceec..67294bfc6 100644 --- a/provider/schema/attribute.go +++ b/provider/schema/attribute.go @@ -10,7 +10,10 @@ import ( // Attribute define a value field inside the Schema. Implementations in this // package include: // - BoolAttribute +// - DynamicAttribute +// - Float32Attribute // - Float64Attribute +// - Int32Attribute // - Int64Attribute // - ListAttribute // - MapAttribute diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index c411062e0..0502821ca 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,9 @@ 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 +} diff --git a/provider/schema/bool_attribute_test.go b/provider/schema/bool_attribute_test.go index 043f2a896..3e134f214 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,29 @@ 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) + } + }) + } +} diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index c738d348d..4b31279f8 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,9 @@ 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 +} diff --git a/provider/schema/dynamic_attribute_test.go b/provider/schema/dynamic_attribute_test.go index c06eaf811..66c7d855e 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() diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index a36c5c435..8e62dc96d 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,9 @@ 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 +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go index c792d326e..70d13db04 100644 --- a/provider/schema/float32_attribute_test.go +++ b/provider/schema/float32_attribute_test.go @@ -9,13 +9,15 @@ 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/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) { @@ -54,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() @@ -102,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() @@ -136,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() @@ -171,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() @@ -205,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() @@ -239,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() @@ -264,17 +254,15 @@ func TestFloat32AttributeGetType(t *testing.T) { attribute: schema.Float32Attribute{}, expected: types.Float32Type, }, - // "custom-type": { - // attribute: schema.Float32Attribute{ - // CustomType: testtypes.Float32Type{}, - // }, - // expected: testtypes.Float32Type{}, - // }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,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() @@ -335,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() @@ -369,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() @@ -403,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() @@ -416,3 +396,29 @@ 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) + } + }) + } +} diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index 786965e3a..f0c0cc008 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,9 @@ 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 +} diff --git a/provider/schema/float64_attribute_test.go b/provider/schema/float64_attribute_test.go index 4478a88b6..8ff796cdb 100644 --- a/provider/schema/float64_attribute_test.go +++ b/provider/schema/float64_attribute_test.go @@ -9,13 +9,15 @@ 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/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) { @@ -54,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() @@ -102,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() @@ -136,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() @@ -171,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() @@ -205,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() @@ -239,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() @@ -264,17 +254,15 @@ func TestFloat64AttributeGetType(t *testing.T) { attribute: schema.Float64Attribute{}, expected: types.Float64Type, }, - // "custom-type": { - // attribute: schema.Float64Attribute{ - // CustomType: testtypes.Float64Type{}, - // }, - // expected: testtypes.Float64Type{}, - // }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -301,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() @@ -335,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() @@ -369,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() @@ -403,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() @@ -416,3 +396,29 @@ 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) + } + }) + } +} diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 16ff58f0f..1f8c60c39 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -182,3 +182,9 @@ 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 +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go index b3ac0687c..b9fa878b7 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,29 @@ 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) + } + }) + } +} diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 3fd9713f1..25243bbf1 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,9 @@ 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 +} diff --git a/provider/schema/int64_attribute_test.go b/provider/schema/int64_attribute_test.go index 505df6657..c5ebec88a 100644 --- a/provider/schema/int64_attribute_test.go +++ b/provider/schema/int64_attribute_test.go @@ -9,13 +9,15 @@ 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/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) { @@ -54,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() @@ -102,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() @@ -137,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() @@ -171,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() @@ -205,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() @@ -230,17 +222,15 @@ func TestInt64AttributeGetType(t *testing.T) { attribute: schema.Int64Attribute{}, expected: types.Int64Type, }, - // "custom-type": { - // attribute: schema.Int64Attribute{ - // CustomType: testtypes.Int64Type{}, - // }, - // expected: testtypes.Int64Type{}, - // }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -273,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() @@ -301,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() @@ -335,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() @@ -369,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() @@ -403,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() @@ -416,3 +396,29 @@ 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) + } + }) + } +} diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index e733b297f..b85848b58 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,12 @@ 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 +} + // 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 cde4bc7c7..d0ab91c58 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() @@ -239,17 +230,15 @@ func TestListAttributeGetType(t *testing.T) { attribute: schema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 0c82da4ad..700299c05 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,12 @@ 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 +} + // 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 93d23a555..1f3715c73 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() @@ -386,17 +375,15 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/list_nested_block_test.go b/provider/schema/list_nested_block_test.go index d8ad26e00..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() @@ -467,17 +453,15 @@ func TestListNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.ListNestedBlock{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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 079535e77..82b5a05d7 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. @@ -23,7 +24,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapAttribute{} ) -// MapAttribute represents a schema attribute that is a list with a single +// MapAttribute represents a schema attribute that is a map with a single // element type. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The ElementType field // must be set. @@ -32,7 +33,7 @@ var ( // require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a list or directly via curly brace syntax. +// return a map or directly via curly brace syntax. // // # map of strings // example_attribute = { @@ -198,6 +199,12 @@ 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 +} + // 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 d16e728c2..f2e1e6a25 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() @@ -239,17 +230,15 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 0cdc9b5e0..14fb4092b 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -24,7 +24,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} ) -// MapNestedAttribute represents an attribute that is a set of objects where +// MapNestedAttribute represents an attribute that is a map of objects where // the object attributes can be fully defined, including further nested // attributes. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The NestedObject field @@ -34,7 +34,7 @@ var ( // not require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a set of objects or directly via curly brace syntax. +// return a map of objects or directly via curly brace syntax. // // # map of objects // example_attribute = { @@ -187,7 +187,7 @@ func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeMap. func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeMap } @@ -223,6 +223,12 @@ 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 +} + // 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 ec0598bf0..bfbdb86c9 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() @@ -386,17 +375,15 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/nested_attribute_object_test.go b/provider/schema/nested_attribute_object_test.go index b1728c532..6a14e6344 100644 --- a/provider/schema/nested_attribute_object_test.go +++ b/provider/schema/nested_attribute_object_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -79,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() @@ -144,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() @@ -184,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() @@ -222,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() @@ -255,17 +248,15 @@ func TestNestedAttributeObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedAttributeObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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 4c310eace..3dd2beaf3 100644 --- a/provider/schema/nested_block_object_test.go +++ b/provider/schema/nested_block_object_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -97,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() @@ -162,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() @@ -202,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() @@ -258,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() @@ -296,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() @@ -341,17 +332,15 @@ func TestNestedBlockObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedBlockObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..bb6ffc6d6 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,12 @@ 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 +} + // 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..80dba9dd1 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() diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index 3041f5a79..c5c81a1ba 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,12 @@ 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 +} + // 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 4fd9c9d9a..a53bc6bf5 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() @@ -245,17 +236,15 @@ func TestObjectAttributeGetType(t *testing.T) { attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, }, - // "custom-type": { - // attribute: schema.ObjectAttribute{ - // CustomType: testtypes.ObjectType{}, - // }, - // expected: testtypes.ObjectType{}, - // }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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() 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..eaf73344e 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,12 @@ 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 +} + // 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 b62366ed9..bfa58193b 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() @@ -239,17 +230,15 @@ func TestSetAttributeGetType(t *testing.T) { attribute: schema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index 64fed1ea2..a9dad64a7 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -183,7 +183,7 @@ func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSet. func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSet } @@ -219,6 +219,12 @@ 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 +} + // 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 32c6b810b..3471f42e3 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() @@ -386,17 +375,15 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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() diff --git a/provider/schema/set_nested_block_test.go b/provider/schema/set_nested_block_test.go index ab0481f14..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() @@ -467,17 +453,15 @@ func TestSetNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SetNestedBlock{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } 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 aac9875af..aded0988e 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -191,7 +191,7 @@ func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject } } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSingle. func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSingle } @@ -233,6 +233,12 @@ 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 +} + // 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 1fba1ce40..cbb2f3aa6 100644 --- a/provider/schema/single_nested_attribute_test.go +++ b/provider/schema/single_nested_attribute_test.go @@ -9,13 +9,15 @@ 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/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) { @@ -80,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() @@ -171,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() @@ -209,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() @@ -247,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() @@ -285,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() @@ -321,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() @@ -354,17 +344,15 @@ func TestSingleNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SingleNestedAttribute{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,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() @@ -433,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() @@ -471,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() @@ -509,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() @@ -523,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() @@ -547,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() diff --git a/provider/schema/single_nested_block_test.go b/provider/schema/single_nested_block_test.go index 9a313bcea..d4a7e75cd 100644 --- a/provider/schema/single_nested_block_test.go +++ b/provider/schema/single_nested_block_test.go @@ -12,6 +12,7 @@ import ( "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/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" @@ -98,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() @@ -150,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() @@ -252,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() @@ -290,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() @@ -328,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() @@ -378,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() @@ -416,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() @@ -461,17 +448,15 @@ func TestSingleNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SingleNestedBlock{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + block: schema.SingleNestedBlock{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..eda7a02c4 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,12 @@ 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 +} + // 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..35e9f03a8 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() 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/schema/attribute.go b/resource/schema/attribute.go index 4a0feceec..67294bfc6 100644 --- a/resource/schema/attribute.go +++ b/resource/schema/attribute.go @@ -10,7 +10,10 @@ import ( // Attribute define a value field inside the Schema. Implementations in this // package include: // - BoolAttribute +// - DynamicAttribute +// - Float32Attribute // - Float64Attribute +// - Int32Attribute // - Int64Attribute // - ListAttribute // - MapAttribute diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index abb0b8708..fa80f565c 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,11 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a BoolAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..9f2d2e863 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() 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..e06600ab4 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,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a DynamicAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..c1340d493 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() 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..3064b4ed9 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,11 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 0b211351b..4671b082f 100644 --- a/resource/schema/float32_attribute_test.go +++ b/resource/schema/float32_attribute_test.go @@ -16,6 +16,7 @@ import ( "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/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -61,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() @@ -122,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() @@ -156,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() @@ -190,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() @@ -224,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() @@ -259,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() @@ -293,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() @@ -327,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() @@ -352,17 +337,15 @@ func TestFloat32AttributeGetType(t *testing.T) { attribute: schema.Float32Attribute{}, expected: types.Float32Type, }, - // "custom-type": { - // attribute: schema.Float32Attribute{ - // CustomType: testtypes.Float32Type{}, - // }, - // expected: testtypes.Float32Type{}, - // }, + "custom-type": { + attribute: schema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,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() @@ -429,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() @@ -463,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() @@ -497,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() @@ -511,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() @@ -561,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() 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..205af3f98 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,11 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 d30f34b0e..0a42d4770 100644 --- a/resource/schema/float64_attribute_test.go +++ b/resource/schema/float64_attribute_test.go @@ -16,6 +16,7 @@ import ( "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/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -61,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() @@ -122,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() @@ -156,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() @@ -190,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() @@ -224,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() @@ -259,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() @@ -293,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() @@ -327,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() @@ -352,17 +337,15 @@ func TestFloat64AttributeGetType(t *testing.T) { attribute: schema.Float64Attribute{}, expected: types.Float64Type, }, - // "custom-type": { - // attribute: schema.Float64Attribute{ - // CustomType: testtypes.Float64Type{}, - // }, - // expected: testtypes.Float64Type{}, - // }, + "custom-type": { + attribute: schema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -395,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() @@ -429,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() @@ -463,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() @@ -497,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() @@ -511,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() @@ -561,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() 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..d3f97d60b 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,11 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..a151c9406 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() 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..c65eb41fa 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,11 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 f52c11928..1ce11e722 100644 --- a/resource/schema/int64_attribute_test.go +++ b/resource/schema/int64_attribute_test.go @@ -16,6 +16,7 @@ import ( "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/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -61,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() @@ -109,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() @@ -144,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() @@ -178,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() @@ -212,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() @@ -237,17 +228,15 @@ func TestInt64AttributeGetType(t *testing.T) { attribute: schema.Int64Attribute{}, expected: types.Int64Type, }, - // "custom-type": { - // attribute: schema.Int64Attribute{ - // CustomType: testtypes.Int64Type{}, - // }, - // expected: testtypes.Int64Type{}, - // }, + "custom-type": { + attribute: schema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -293,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() @@ -327,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() @@ -361,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() @@ -395,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() @@ -429,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() @@ -463,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() @@ -497,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() @@ -511,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() @@ -561,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() 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..9c1536dbe 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,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 5c15849cc..1be15bbb7 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() @@ -244,17 +234,15 @@ func TestListAttributeGetType(t *testing.T) { attribute: schema.ListAttribute{ElementType: types.StringType}, expected: types.ListType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.ListAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } 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() diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 95fd2ba01..ee1845bb5 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,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue returns the Default field value. func (a ListNestedAttribute) ListDefaultValue() defaults.List { return a.Default @@ -284,6 +302,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 1662109aa..258d7cf44 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -90,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() @@ -144,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() @@ -235,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() @@ -275,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() @@ -315,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() @@ -353,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() @@ -390,17 +379,15 @@ func TestListNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.ListNestedAttribute{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + attribute: schema.ListNestedAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -439,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() @@ -479,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() @@ -519,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() @@ -559,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() @@ -573,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() @@ -620,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() @@ -660,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() @@ -700,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() @@ -908,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() diff --git a/resource/schema/list_nested_block_test.go b/resource/schema/list_nested_block_test.go index 332a7ad19..e500287c1 100644 --- a/resource/schema/list_nested_block_test.go +++ b/resource/schema/list_nested_block_test.go @@ -14,6 +14,7 @@ import ( "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/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -86,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() @@ -140,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() @@ -260,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() @@ -300,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() @@ -340,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() @@ -378,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() @@ -418,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() @@ -458,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() @@ -507,17 +492,15 @@ func TestListNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.ListNestedBlock{ - // CustomType: testtypes.ListType{}, - // }, - // expected: testtypes.ListType{}, - // }, + "custom-type": { + block: schema.ListNestedBlock{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -568,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 ea08fa7b2..f76a295c3 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -28,7 +28,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapAttribute{} ) -// MapAttribute represents a schema attribute that is a list with a single +// MapAttribute represents a schema attribute that is a map with a single // element type. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The ElementType field // must be set. @@ -37,7 +37,7 @@ var ( // require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a list or directly via curly brace syntax. +// return a map or directly via curly brace syntax. // // # map of strings // example_attribute = { @@ -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,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 e4ff058f0..b62293350 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() @@ -244,17 +234,15 @@ func TestMapAttributeGetType(t *testing.T) { attribute: schema.MapAttribute{ElementType: types.StringType}, expected: types.MapType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.MapAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } 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() diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index faa1f3276..db868f726 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -29,7 +29,7 @@ var ( _ fwxschema.AttributeWithMapValidators = MapNestedAttribute{} ) -// MapNestedAttribute represents an attribute that is a set of objects where +// MapNestedAttribute represents an attribute that is a map of objects where // the object attributes can be fully defined, including further nested // attributes. When retrieving the value for this attribute, use types.Map // as the value type unless the CustomType field is set. The NestedObject field @@ -39,7 +39,7 @@ var ( // not require definition beyond type information. // // Terraform configurations configure this attribute using expressions that -// return a set of objects or directly via curly brace syntax. +// return a map of objects or directly via curly brace syntax. // // # map of objects // example_attribute = { @@ -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 @@ -224,7 +237,7 @@ func (a MapNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeMap. func (a MapNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeMap } @@ -260,6 +273,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue returns the Default field value. func (a MapNestedAttribute) MapDefaultValue() defaults.Map { return a.Default @@ -284,6 +302,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 ba3e1cdd7..6f360d85b 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -90,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() @@ -144,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() @@ -235,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() @@ -275,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() @@ -315,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() @@ -353,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() @@ -390,17 +379,15 @@ func TestMapNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.MapNestedAttribute{ - // CustomType: testtypes.MapType{}, - // }, - // expected: testtypes.MapType{}, - // }, + "custom-type": { + attribute: schema.MapNestedAttribute{ + CustomType: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, + expected: testtypes.MapType{MapType: types.MapType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -439,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() @@ -479,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() @@ -519,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() @@ -559,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() @@ -573,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() @@ -620,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() @@ -660,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() @@ -700,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() @@ -908,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() 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 af2156cbb..f86c1952e 100644 --- a/resource/schema/nested_attribute_object_test.go +++ b/resource/schema/nested_attribute_object_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -80,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() @@ -145,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() @@ -185,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() @@ -223,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() @@ -260,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() @@ -293,17 +284,15 @@ func TestNestedAttributeObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedAttributeObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedAttributeObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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 830a9d3f8..81e64c4e0 100644 --- a/resource/schema/nested_block_object_test.go +++ b/resource/schema/nested_block_object_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -98,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() @@ -163,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() @@ -203,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() @@ -259,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() @@ -297,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() @@ -335,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() @@ -380,17 +369,15 @@ func TestNestedBlockObjectType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.NestedBlockObject{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + object: schema.NestedBlockObject{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..8f367592e 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,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a NumberAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..537bd1bfc 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() 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..03f35aa00 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,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ObjectAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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 ddc014742..908c48395 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() @@ -250,17 +240,15 @@ func TestObjectAttributeGetType(t *testing.T) { attribute: schema.ObjectAttribute{AttributeTypes: map[string]attr.Type{"testattr": types.StringType}}, expected: types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, }, - // "custom-type": { - // attribute: schema.ObjectAttribute{ - // CustomType: testtypes.ObjectType{}, - // }, - // expected: testtypes.ObjectType{}, - // }, + "custom-type": { + attribute: schema.ObjectAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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() 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..b65656dac 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -230,6 +230,11 @@ 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 +} + // 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 c5675e820..2795fbde8 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -62,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() @@ -110,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() @@ -150,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() @@ -184,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() @@ -218,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() @@ -243,17 +234,15 @@ func TestSetAttributeGetType(t *testing.T) { attribute: schema.SetAttribute{ElementType: types.StringType}, expected: types.SetType{ElemType: types.StringType}, }, - // "custom-type": { - // attribute: schema.SetAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -286,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() @@ -320,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() @@ -354,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() @@ -388,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() @@ -402,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() @@ -449,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() @@ -483,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() @@ -517,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() @@ -702,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() diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index 3f2b3d1bb..5d408cf1a 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -219,7 +219,7 @@ func (a SetNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject { return a.NestedObject } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSet. func (a SetNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSet } @@ -255,6 +255,11 @@ 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 +} + // SetDefaultValue returns the Default field value. func (a SetNestedAttribute) SetDefaultValue() defaults.Set { return a.Default @@ -279,6 +284,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 109921d00..a0210f410 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -90,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() @@ -144,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() @@ -235,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() @@ -275,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() @@ -315,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() @@ -353,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() @@ -390,17 +379,15 @@ func TestSetNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SetNestedAttribute{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + attribute: schema.SetNestedAttribute{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -439,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() @@ -479,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() @@ -519,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() @@ -559,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() @@ -573,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() @@ -620,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() @@ -660,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() @@ -700,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() @@ -908,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() 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 f90edb279..409e5d0e5 100644 --- a/resource/schema/set_nested_block_test.go +++ b/resource/schema/set_nested_block_test.go @@ -10,16 +10,18 @@ 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" "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/schema" "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) { @@ -86,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() @@ -140,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() @@ -260,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() @@ -300,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() @@ -340,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() @@ -378,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() @@ -418,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() @@ -458,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() @@ -507,17 +493,15 @@ func TestSetNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SetNestedBlock{ - // CustomType: testtypes.SetType{}, - // }, - // expected: testtypes.SetType{}, - // }, + "custom-type": { + block: schema.SetNestedBlock{ + CustomType: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, + expected: testtypes.SetType{SetType: types.SetType{ElemType: types.StringType}}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -565,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 a47ef6541..12cab9800 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 @@ -229,7 +239,7 @@ func (a SingleNestedAttribute) GetNestedObject() fwschema.NestedAttributeObject } } -// GetNestingMode always returns NestingModeList. +// GetNestingMode always returns NestingModeSingle. func (a SingleNestedAttribute) GetNestingMode() fwschema.NestingMode { return fwschema.NestingModeSingle } @@ -271,6 +281,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue returns the Default field value. func (a SingleNestedAttribute) ObjectDefaultValue() defaults.Object { return a.Default @@ -295,6 +310,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 ba9fb1309..b03725b76 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" @@ -88,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() @@ -179,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() @@ -217,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() @@ -255,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() @@ -293,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() @@ -329,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() @@ -362,17 +351,15 @@ func TestSingleNestedAttributeGetType(t *testing.T) { }, }, }, - // "custom-type": { - // attribute: schema.SingleNestedAttribute{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + attribute: schema.SingleNestedAttribute{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } for name, testCase := range testCases { - name, testCase := name, testCase - t.Run(name, func(t *testing.T) { t.Parallel() @@ -409,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() @@ -447,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() @@ -485,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() @@ -523,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() @@ -537,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() @@ -588,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() @@ -626,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() @@ -664,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() @@ -818,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() diff --git a/resource/schema/single_nested_block_test.go b/resource/schema/single_nested_block_test.go index 7a18414f9..5c2c7a0c5 100644 --- a/resource/schema/single_nested_block_test.go +++ b/resource/schema/single_nested_block_test.go @@ -12,6 +12,7 @@ import ( "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/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -99,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() @@ -151,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() @@ -253,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() @@ -291,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() @@ -329,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() @@ -379,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() @@ -417,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() @@ -455,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() @@ -500,17 +485,15 @@ func TestSingleNestedBlockType(t *testing.T) { }, }, }, - // "custom-type": { - // block: schema.SingleNestedBlock{ - // CustomType: testtypes.SingleType{}, - // }, - // expected: testtypes.SingleType{}, - // }, + "custom-type": { + block: schema.SingleNestedBlock{ + CustomType: testtypes.ObjectType{}, + }, + expected: testtypes.ObjectType{}, + }, } 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..693327016 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,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a StringAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // 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..6bf70eda4 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() 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/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.go b/tfsdk/ephemeral_result_data.go new file mode 100644 index 000000000..b3990ca77 --- /dev/null +++ b/tfsdk/ephemeral_result_data.go @@ -0,0 +1,94 @@ +// 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" +) + +// EphemeralResultData represents the data returned after opening a Terraform ephemeral resource. +type EphemeralResultData struct { + Raw tftypes.Value + Schema fwschema.Schema +} + +// Get populates the struct passed as `target` with the entire ephemeral result data object. +func (s EphemeralResultData) Get(ctx context.Context, target interface{}) diag.Diagnostics { + return s.data().Get(ctx, target) +} + +// GetAttribute retrieves the attribute or block found at `path` and populates +// the `target` with the value. This method is intended for top level schema +// attributes or blocks. Use `types` package methods or custom types to step +// into collections. +// +// Attributes or elements under null or unknown collections return null +// values, however this behavior is not protected by compatibility promises. +func (s EphemeralResultData) 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 EphemeralResultData) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return s.data().PathMatches(ctx, pathExpr) +} + +// Set populates the entire ephemeral result data object 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 *EphemeralResultData) 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, including any parent attribute +// paths as necessary. +// +// 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 *EphemeralResultData) SetAttribute(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.SetAtPath(ctx, path, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +func (s EphemeralResultData) data() *fwschemadata.Data { + return &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionEphemeralResultData, + Schema: s.Schema, + TerraformValue: s.Raw, + } +} diff --git a/tfsdk/ephemeral_result_data_test.go b/tfsdk/ephemeral_result_data_test.go new file mode 100644 index 000000000..4e2cbb4da --- /dev/null +++ b/tfsdk/ephemeral_result_data_test.go @@ -0,0 +1,480 @@ +// 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 TestEphemeralResultDataGet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ephemeralResultData tfsdk.EphemeralResultData + target any + expected any + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataGet for more exhaustive unit testing. + // These test cases are to ensure EphemeralResultData schema and data values are + // passed appropriately to the shared implementation. + "valid": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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{ + Optional: true, + Type: types.StringType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"string"` + }), + expected: &struct { + String types.String `tfsdk:"string"` + }{ + String: types.StringValue("test"), + }, + }, + "diagnostic": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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{ + Optional: 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.ephemeralResultData.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 TestEphemeralResultDataGetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralResultData tfsdk.EphemeralResultData + 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 EphemeralResultData schema and data values + // are passed appropriately to the shared implementation. + "valid": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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, + Required: true, + }, + }, + }, + }, + target: new(string), + expected: pointer("namevalue"), + }, + "diagnostics": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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{}, + Required: 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.ephemeralResultData.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 TestEphemeralResultDataPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + ephemeralResultData tfsdk.EphemeralResultData + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataPathMatches for more exhaustive unit testing. + // These test cases are to ensure EphemeralResultData schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + ephemeralResultData: tfsdk.EphemeralResultData{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + }, + }, + }, + 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": { + ephemeralResultData: tfsdk.EphemeralResultData{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + }, + }, + }, + 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.ephemeralResultData.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 TestEphemeralResultDataSet(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralResultData tfsdk.EphemeralResultData + 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 EphemeralResultData schema and data values are + // passed appropriately to the shared implementation. + "valid": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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, + Required: 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": { + ephemeralResultData: tfsdk.EphemeralResultData{ + Raw: tftypes.Value{}, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + Required: 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.ephemeralResultData.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.ephemeralResultData.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestEphemeralResultDataSetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + ephemeralResultData tfsdk.EphemeralResultData + 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 EphemeralResultData schema and data values + // are passed appropriately to the shared implementation. + "valid": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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, + Required: true, + }, + "other": testschema.Attribute{ + Type: types.StringType, + Required: 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": { + ephemeralResultData: tfsdk.EphemeralResultData{ + 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{}, + Required: 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.ephemeralResultData.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.ephemeralResultData.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} 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/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 c0b60b613..c7560ffca 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,24 +1,23 @@ module tools -go 1.21 +go 1.22.7 -require github.com/hashicorp/copywrite v0.19.0 +require github.com/hashicorp/copywrite v0.20.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.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // 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.21.0 // indirect + golang.org/x/crypto v0.31.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.33.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.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 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 dc4eeb8c6..e9c0dc344 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= @@ -97,8 +95,9 @@ github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 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/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= @@ -145,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.20.0 h1:i+iNq4lWsGopKIhC0HfZjUvNAnXnU/Pc5e+4L5WF+1Y= +github.com/hashicorp/copywrite v0.20.0/go.mod h1:mu6DAyUI6m6vq8weoJn9a0HDuUUrV+0GQdRp4mD50yU= 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= @@ -185,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= @@ -225,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= @@ -242,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= @@ -277,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= @@ -318,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= @@ -356,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= @@ -385,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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +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/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= @@ -420,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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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= @@ -442,8 +430,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 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/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= @@ -477,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= @@ -494,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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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= @@ -516,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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/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= @@ -576,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/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index b1fb0c9a6..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" } ] }, @@ -265,6 +269,35 @@ } ] }, + { + "title": "Ephemeral Resources", + "routes": [ + { + "title": "Overview", + "path": "ephemeral-resources" + }, + { + "title": "Open", + "path": "ephemeral-resources/open" + }, + { + "title": "Configure Clients", + "path": "ephemeral-resources/configure" + }, + { + "title": "Validate Configuration", + "path": "ephemeral-resources/validate-configuration" + }, + { + "title": "Renew", + "path": "ephemeral-resources/renew" + }, + { + "title": "Close", + "path": "ephemeral-resources/close" + } + ] + }, { "title": "Handling Data", "routes": [ diff --git a/website/docs/plugin/framework/acctests.mdx b/website/docs/plugin/framework/acctests.mdx index fc47808d2..e23cd6fbb 100644 --- a/website/docs/plugin/framework/acctests.mdx +++ b/website/docs/plugin/framework/acctests.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Acceptance Tests' +page_title: Acceptance tests description: >- - How to write acceptance tests for providers built on the framework. Acceptance - tests imitate applying configuration files. + Learn how to write acceptance tests for providers built on the framework. + Acceptance tests help ensure your provider works as expected by imitating + Terraform operations. --- -# Acceptance Tests +# 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 0fac9a737..11dda382b 100644 --- a/website/docs/plugin/framework/data-sources/configure.mdx +++ b/website/docs/plugin/framework/data-sources/configure.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Configure Data Sources' +page_title: Configure data sources description: >- - How to configure data sources with provider data or clients in the provider development framework. + Learn how to configure data sources with provider data or clients in the + Terraform plugin framework. --- -# Configure Data Sources +# 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 823f8c311..64dfb2ecc 100644 --- a/website/docs/plugin/framework/data-sources/index.mdx +++ b/website/docs/plugin/framework/data-sources/index.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Data Sources' +page_title: Data sources description: >- - How to build data sources in the provider development framework. Data sources - allow Terraform to reference external data. + Data sources allow Terraform to reference external data. Learn how the + framework can help you implement data sources. --- -# Data Sources +# 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 1472f0e68..7868dd402 100644 --- a/website/docs/plugin/framework/data-sources/timeouts.mdx +++ b/website/docs/plugin/framework/data-sources/timeouts.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Timeouts' +page_title: Timeouts description: >- - How to use timeouts with the provider development framework. + Learn how to implement timeouts with the Terraform plugin framework. --- # Timeouts diff --git a/website/docs/plugin/framework/data-sources/validate-configuration.mdx b/website/docs/plugin/framework/data-sources/validate-configuration.mdx index 861f1ccc7..0a0a55dc6 100644 --- a/website/docs/plugin/framework/data-sources/validate-configuration.mdx +++ b/website/docs/plugin/framework/data-sources/validate-configuration.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Validate Data Source Configurations' +page_title: Validate data source configurations description: >- - How to validate data source configurations with the provider development framework. + Learn how to validate data source configurations with the Terraform plugin + framework. --- -# Validate Configuration +# 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 9bc824ae6..ddae1484b 100644 --- a/website/docs/plugin/framework/debugging.mdx +++ b/website/docs/plugin/framework/debugging.mdx @@ -1,9 +1,10 @@ --- -page_title: Plugin Development - Debugging Framework Providers -description: How to implement debugger support in Framework Terraform providers. +page_title: Debugging framework providers +description: >- + Learn how to implement debugger support in framework Terraform providers. --- -# Debugging Framework Providers +# 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 017a4eaa1..c86ce0ae0 100644 --- a/website/docs/plugin/framework/deprecations.mdx +++ b/website/docs/plugin/framework/deprecations.mdx @@ -1,9 +1,11 @@ --- -page_title: 'Plugin Development - Deprecations, Removals, and Renames Best Practices' -description: 'Recommendations for deprecations, removals, and renames.' +page_title: Deprecations, removals, and renames +description: + Use the following recommendations to handle deprecations, removals, and + renames in framework providers. --- -# Deprecations, Removals, and Renames +# 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 8a87d4326..7208d4845 100644 --- a/website/docs/plugin/framework/diagnostics.mdx +++ b/website/docs/plugin/framework/diagnostics.mdx @@ -1,11 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Errors and Warnings' -description: |- - How to return errors and warnings from the Terraform provider development - framework. +page_title: Errors and warnings +description: >- + Learn how to return errors and warnings from the Terraform plugin framework. --- -# Returning Errors and Warnings +# Returning errors and warnings Providers use `Diagnostics` to surface errors and warnings to practitioners, such as contextual messages returned from Terraform CLI at the end of diff --git a/website/docs/plugin/framework/ephemeral-resources/close.mdx b/website/docs/plugin/framework/ephemeral-resources/close.mdx new file mode 100644 index 000000000..dade8f2bd --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/close.mdx @@ -0,0 +1,94 @@ +--- +page_title: Closing ephemeral resources +description: >- + Learn how to close ephemeral resource in the Terraform plugin framework. +--- + +# 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. + +`Close` is an optional lifecycle implementation for an ephemeral resource, other lifecycle implementations include: + +- [Open](/terraform/plugin/framework/ephemeral-resources/open) an ephemeral resource by receiving Terraform configuration, retrieving a remote object, and returning ephemeral result data to Terraform. +- [Renew](/terraform/plugin/framework/ephemeral-resources/renew) an expired remote object at a specified time. + +## Define Close Method + +The [`ephemeral.EphemeralResourceWithClose` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithClose) on the [`ephemeral.EphemeralResource` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource) implementation will enable close support for an ephemeral resource. + +Implement the `Close` method by: + +1. [Accessing private data](/terraform/plugin/framework/resources/private-state#reading-private-state-data) from [`ephemeral.CloseRequest.Private` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#CloseRequest.Private) needed to close the remote object. +1. Performing logic or external calls to close the remote object. + +If the logic needs to return [warning or error diagnostics](/terraform/plugin/framework/diagnostics), they can be added into the [`ephemeral.CloseResponse.Diagnostics` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#CloseResponse.Diagnostics). + +In this example, an ephemeral resource named `examplecloud_thing` with hardcoded behavior is defined. `Private` data needed to execute `Close` is passed from the `Open` response: + +```go +var _ ephemeral.EphemeralResourceWithClose = (*ThingEphemeralResource)(nil) + +// ThingEphemeralResource defines the ephemeral resource implementation, which also implements Close. +type ThingEphemeralResource struct{} + +type ThingEphemeralResourceModel struct { + Name types.String `tfsdk:"name"` + Token types.String `tfsdk:"token"` +} + +type ThingPrivateData struct { + Name string `json:"name"` +} + +func (e *ThingEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name of the thing to retrieve a token for.", + Required: true, + }, + "token": schema.StringAttribute{ + Description: "Token for the thing.", + Computed: true, + }, + }, + } +} + +func (e *ThingEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ThingEphemeralResourceModel + + // Read Terraform config data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Typically ephemeral resources will make external calls and reference returned data, + // however this example hardcodes the setting of result and private data for brevity. + data.Token = types.StringValue("token-123") + + // When closing, pass along this data (error handling omitted for brevity). + privateData, _ := json.Marshal(ThingPrivateData{Name: data.Name.ValueString()}) + resp.Private.SetKey(ctx, "thing_data", privateData) + + // Save data into ephemeral result data + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} + +func (e *ThingEphemeralResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + privateBytes, diags := req.Private.GetKey(ctx, "thing_data") + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Unmarshal private data (error handling omitted for brevity). + var privateData ThingPrivateData + json.Unmarshal(privateBytes, &privateData) + + // Perform external call to close/clean up "thing" data +} + +``` diff --git a/website/docs/plugin/framework/ephemeral-resources/configure.mdx b/website/docs/plugin/framework/ephemeral-resources/configure.mdx new file mode 100644 index 000000000..fef8e4a12 --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/configure.mdx @@ -0,0 +1,104 @@ +--- +page_title: Configuring ephemeral resources +description: >- + Learn how to configure ephemeral resources with provider data or clients in + the Terraform plugin framework. +--- + +# 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. + +## Prepare Provider Configure Method + +Implement the [`provider.ConfigureResponse.EphemeralResourceData` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ConfigureResponse.EphemeralResourceData) in the [`Provider` interface `Configure` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Configure). This value can be set to any type, whether an existing client or vendor SDK type, a provider-defined custom type, or the provider implementation itself. It is recommended to use pointer types so that ephemeral resources can determine if this value was configured before attempting to use it. + +During execution of the [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands, Terraform calls the [`ConfigureProvider`](/terraform/plugin/framework/internals/rpcs#configureprovider-rpc) RPC, in which the framework calls the [`provider.Provider` interface `Configure` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Configure). + +In this example, the Go standard library [`net/http.Client`](https://pkg.go.dev/net/http#Client) is configured in the provider, and made available for ephemeral resources: + +```go +// With the provider.Provider implementation +func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.EphemeralResourceData = &http.Client{/* ... */} +} +``` + +In this example, the code defines an `ExampleClient` type that is made available for ephemeral resources: + +```go +type ExampleClient struct { + /* ... */ +} + +// With the provider.Provider implementation +func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.EphemeralResourceData = &ExampleClient{/* ... */} +} +``` + +In this example, the `ExampleCloudProvider` type itself is made available for ephemeral resources: + +```go +// With the provider.Provider implementation +type ExampleCloudProvider struct { + /* ... */ +} + +func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + resp.EphemeralResourceData = p +} +``` + +## Define Ephemeral Resource Configure Method + +Implement the [`ephemeral.EphemeralResourceWithConfigure` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithConfigure) which receives the provider configured data from the [`Provider` interface `Configure` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Configure) and saves it into the [`ephemeral.EphemeralResource` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource) implementation. + +The [`ephemeral.EphemeralResourceWithConfigure` interface `Configure` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithConfigure.Configure) is called during execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands when the `ValidateEphemeralResourceConfig` RPC is sent. Additionally, the [`ephemeral.EphemeralResourceWithConfigure` interface `Configure` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithConfigure.Configure) is called during execution of the [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands when the `OpenEphemeralResource` RPC is sent. + +-> Note that Terraform calling the `ValidateEphemeralResourceConfig` RPC would not call the [`ConfigureProvider`](/terraform/plugin/framework/internals/rpcs#configureprovider-rpc) RPC first, so implementations need to account for that situation. Configuration validation in Terraform occurs without provider configuration ("offline"). + +In this example, the provider configured the Go standard library [`net/http.Client`](https://pkg.go.dev/net/http#Client) which the ephemeral resource uses during `Open`: + +```go +// With the ephemeral.EphemeralResource implementation +type ThingEphemeralResource struct { + client *http.Client +} + +func (d *ThingEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { + // Always perform a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*http.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Ephemeral Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = client +} + +func (d *ThingEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + // Prevent panic if the provider has not been configured. + if d.client == nil { + resp.Diagnostics.AddError( + "Unconfigured HTTP Client", + "Expected configured HTTP client. Please report this issue to the provider developers.", + ) + + return + } + + httpResp, err := d.client.Get("https://example.com") + /* ... */ +} +``` diff --git a/website/docs/plugin/framework/ephemeral-resources/index.mdx b/website/docs/plugin/framework/ephemeral-resources/index.mdx new file mode 100644 index 000000000..194d65d5f --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/index.mdx @@ -0,0 +1,101 @@ +--- +page_title: Ephemeral resources +description: >- + Ephemeral resources allow Terraform to reference external data, while + guaranteeing that this data will not be persisted in plan or state. Learn how + to implement ephemeral resources in the Terraform plugin framework. +--- + +# Ephemeral resources + + + +Ephemeral resource support is in technical preview and offered without compatibility promises until Terraform 1.10 is generally available. + + + +[Ephemeral resources](/terraform/language/v1.10.x/resources/ephemeral) are an abstraction that allows Terraform to reference external data. Unlike [data sources](/terraform/language/data-sources), Terraform guarantees that ephemeral resource data will not be persisted in plan or state artifacts. The data produced by an ephemeral resource can only be referenced in [specific ephemeral contexts](/terraform/language/v1.10.x/resources/ephemeral#referencing-ephemeral-resources) or Terraform will throw an error. + +This page describes the basic implementation details required for supporting an ephemeral resource within the provider. Ephemeral resources, as a part of their lifecycle, must implement: + +- [Open](/terraform/plugin/framework/ephemeral-resources/open) an ephemeral resource by receiving Terraform configuration, retrieving a remote object, and returning ephemeral result data to Terraform. + +Further documentation is available for deeper ephemeral resource concepts: + +- [Configure](/terraform/plugin/framework/ephemeral-resources/configure) an ephemeral resource with provider-level data types or clients. +- [Validate](/terraform/plugin/framework/ephemeral-resources/validate-configuration) practitioner configuration against acceptable values. +- [Renew](/terraform/plugin/framework/ephemeral-resources/renew) an expired remote object at a specified time. +- [Close](/terraform/plugin/framework/ephemeral-resources/close) a remote object when Terraform no longer needs the data. + +## Define Ephemeral Resource Type + +Implement the [`ephemeral.EphemeralResource` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource). Ensure the [Add Ephemeral Resource To Provider](#add-ephemeral-resource-to-provider) documentation is followed so the ephemeral resource becomes part of the provider implementation, and therefore available to practitioners. + +### Metadata Method + +The [`ephemeral.EphemeralResource` interface `Metadata` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource.Metadata) defines the ephemeral resource name as it would appear in Terraform configurations. This name should include the provider type prefix, an underscore, then the ephemeral resource specific name. For example, a provider named `examplecloud` and an ephemeral resource that reads "thing" ephemeral data would be named `examplecloud_thing`. + +In this example, the ephemeral resource name in an `examplecloud` provider that reads "thing" ephemeral resource data is hardcoded to `examplecloud_thing`: + +```go +// With the ephemeral.EphemeralResource implementation +func (r *ThingEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = "examplecloud_thing" +} +``` + +To simplify ephemeral resource implementations, the [`provider.MetadataResponse.TypeName` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#MetadataResponse.TypeName) from the [`provider.Provider` interface `Metadata` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Metadata) can set the provider name so it is available in the [`ephemeral.MetadataRequest.ProviderTypeName` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#MetadataRequest.ProviderTypeName). + +In this example, the provider defines the `examplecloud` name for itself, and the ephemeral resource is named `examplecloud_thing`: + +```go +// With the provider.Provider implementation +func (p *ExampleCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "examplecloud" +} + +// With the ephemeral.EphemeralResource implementation +func (d *ThingEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_thing" +} +``` + +### Schema Method + +The [`ephemeral.EphemeralResource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource.Schema) defines a [schema](/terraform/plugin/framework/handling-data/schemas) describing what data is available in the ephemeral resource's configuration and result data. + +## Add Ephemeral Resource to Provider + +Ephemeral resources become available to practitioners when they are included in the [provider](/terraform/plugin/framework/providers) implementation via the optional [`provider.ProviderWithEphemeralResources` interface `EphemeralResources` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ProviderWithEphemeralResources.EphemeralResource). + +In this example, the `ThingEphemeralResource` type, which implements the `ephemeral.EphemeralResource` interface, is added to the provider implementation: + +```go +var _ provider.ProviderWithEphemeralResources = (*ExampleCloudProvider)(nil) + +func (p *ExampleCloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + func() ephemeral.EphemeralResource { + return &ThingResource{}, + }, + } +} +``` + +To simplify provider implementations, a named function can be created with the ephemeral resource implementation. + +In this example, the `ThingEphemeralResource` code includes an additional `NewThingEphemeralResource` function, which simplifies the provider implementation: + +```go +// With the provider.ProviderWithEphemeralResources implementation +func (p *ExampleCloudProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + NewThingEphemeralResource, + } +} + +// With the ephemeral.EphemeralResource implementation +func NewThingEphemeralResource() ephemeral.EphemeralResource { + return &ThingEphemeralResource{} +} +``` diff --git a/website/docs/plugin/framework/ephemeral-resources/open.mdx b/website/docs/plugin/framework/ephemeral-resources/open.mdx new file mode 100644 index 000000000..3f7ff646a --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/open.mdx @@ -0,0 +1,76 @@ +--- +page_title: Opening ephemeral resources +description: >- + Learn how to open ephemeral resource in the Terraform plugin framework. +--- + +# 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. + +`Open` is the only required lifecycle implementation for an ephemeral resource, optional lifecycle implementations include: + +- [Renew](/terraform/plugin/framework/ephemeral-resources/renew) an expired remote object at a specified time. +- [Close](/terraform/plugin/framework/ephemeral-resources/close) a remote object when Terraform no longer needs the data. + +## Define Open Method + +Implement the `Open` method by: + +1. [Accessing the `Config` data](/terraform/plugin/framework/handling-data/accessing-values) from the [`ephemeral.OpenRequest` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenRequest). +1. Performing logic or external calls to read the result data for the ephemeral resource. +1. Determining if a remote object needs to be renewed, setting the [`ephemeral.OpenResponse.RenewAt` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenResponse.RenewAt) to indicate to Terraform when to call the provider [`Renew`](/terraform/plugin/framework/ephemeral-resources/renew) method. +1. [Writing private data](/terraform/plugin/framework/resources/private-state#saving-private-state-data) needed to `Renew` or `Close` the ephemeral resource to the [`ephemeral.OpenResponse.Private` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenResponse.Private). +1. [Writing result data](/terraform/plugin/framework/writing-state) into the [`ephemeral.OpenResponse.Result` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenResponse.Result). + +If the logic needs to return [warning or error diagnostics](/terraform/plugin/framework/diagnostics), they can be added into the [`ephemeral.OpenResponse.Diagnostics` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenResponse.Diagnostics). + +In this example, an ephemeral resource named `examplecloud_thing` with hardcoded behavior is defined: + +```go +// ThingEphemeralResource defines the ephemeral resource implementation. +// Some ephemeral.EphemeralResource interface methods are omitted for brevity. +type ThingEphemeralResource struct {} + +type ThingEphemeralResourceModel struct { + Name types.String `tfsdk:"name"` + Token types.String `tfsdk:"token"` +} + +func (e *ThingEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name of the thing to retrieve a token for.", + Required: true, + }, + "token": schema.StringAttribute{ + Description: "Token for the thing.", + Computed: true, + }, + }, + } +} + +func (e *ThingEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ThingEphemeralResourceModel + + // Read Terraform config data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Typically ephemeral resources will make external calls, however this example + // hardcodes setting the token attribute to a specific value for brevity. + data.Token = types.StringValue("token-123") + + // Save data into ephemeral result data + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} +``` + +## Caveats + +* An error is returned if the `Result` data contains unknown values. Set all attributes to either null or known values in the response. +* An error is returned unless every non-computed known value in the request config is saved exactly as-is into the result data. Only null values marked as computed can be modified. diff --git a/website/docs/plugin/framework/ephemeral-resources/renew.mdx b/website/docs/plugin/framework/ephemeral-resources/renew.mdx new file mode 100644 index 000000000..dadd729f7 --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/renew.mdx @@ -0,0 +1,113 @@ +--- +page_title: Renewing ephemeral resources +description: >- + Learn how to renew ephemeral resource in the Terraform plugin framework. +--- + +# 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. + + + +`Renew` cannot return new result data for the ephemeral resource instance, so this logic is only appropriate for remote objects like HashiCorp Vault leases, which can be renewed without changing their data. + + + +`Renew` is an optional lifecycle implementation for an ephemeral resource, other lifecycle implementations include: + +- [Open](/terraform/plugin/framework/ephemeral-resources/open) an ephemeral resource by receiving Terraform configuration, retrieving a remote object, and returning ephemeral result data to Terraform. +- [Close](/terraform/plugin/framework/ephemeral-resources/close) a remote object when Terraform no longer needs the data. + +## Define Renew Method + +The [`ephemeral.EphemeralResourceWithRenew` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithRenew) on the [`ephemeral.EphemeralResource` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource) implementation will enable renew support for an ephemeral resource. + +Implement the `Renew` method by: + +1. [Accessing private data](/terraform/plugin/framework/resources/private-state#reading-private-state-data) from [`ephemeral.RenewRequest.Private` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#RenewRequest.Private) needed to renew the remote object. +1. Performing logic or external calls to renew the remote object. +1. Determining if a remote object needs to be renewed again, setting the [`ephemeral.RenewResponse.RenewAt` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#RenewResponse.RenewAt) to indicate to Terraform when to call the provider [`Renew`](/terraform/plugin/framework/ephemeral-resources/renew) method. +1. [Writing private data](/terraform/plugin/framework/resources/private-state#saving-private-state-data) needed to `Renew` or `Close` the ephemeral resource to the [`ephemeral.RenewResponse.Private` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#RenewResponse.Private). + +If the logic needs to return [warning or error diagnostics](/terraform/plugin/framework/diagnostics), they can be added into the [`ephemeral.RenewResponse.Diagnostics` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#RenewResponse.Diagnostics). + +In this example, an ephemeral resource named `examplecloud_thing` with hardcoded behavior is defined. It indicates a renewal should occur 5 minutes from when either the `Open` or `Renew` method is executed: + +```go +var _ ephemeral.EphemeralResourceWithRenew = (*ThingEphemeralResource)(nil) + +// ThingEphemeralResource defines the ephemeral resource implementation, which also implements Renew. +type ThingEphemeralResource struct{} + +type ThingEphemeralResourceModel struct { + Name types.String `tfsdk:"name"` + Token types.String `tfsdk:"token"` +} + +type ThingPrivateData struct { + Name string `json:"name"` +} + +func (e *ThingEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "Name of the thing to retrieve a token for.", + Required: true, + }, + "token": schema.StringAttribute{ + Description: "Token for the thing.", + Computed: true, + }, + }, + } +} + +func (e *ThingEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ThingEphemeralResourceModel + + // Read Terraform config data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Typically ephemeral resources will make external calls and reference returned data, + // however this example hardcodes the setting of result and private data for brevity. + data.Token = types.StringValue("token-123") + + // Renew 5 minutes from now + resp.RenewAt = time.Now().Add(5 * time.Minute) + + // When renewing, pass along this data (error handling omitted for brevity). + privateData, _ := json.Marshal(ThingPrivateData{Name: data.Name.ValueString()}) + resp.Private.SetKey(ctx, "thing_data", privateData) + + // Save data into ephemeral result data + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} + +func (e *ThingEphemeralResource) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) { + privateBytes, _ := req.Private.GetKey(ctx, "thing_data") + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Unmarshal private data (error handling omitted for brevity). + var privateData ThingPrivateData + json.Unmarshal(privateBytes, &privateData) + + // Perform external call to renew "thing" data + + // Renew again in 5 minutes + resp.RenewAt = time.Now().Add(5 * time.Minute) + + // If needed, you can also set new `Private` data on the response. +} +``` + +## Recommendations + +* When setting the `RenewAt` response field, add extra time (usually no more than a few minutes) before an ephemeral resource expires to account for latency. diff --git a/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx b/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx new file mode 100644 index 000000000..242dd4cfc --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx @@ -0,0 +1,86 @@ +--- +page_title: Validate ephemeral resource configurations +description: >- + Learn how to validate ephemeral resource configurations with the Terraform + plugin framework. +--- + +# 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). + +This page describes implementation details for validating entire ephemeral resource configurations, typically referencing multiple attributes. Further documentation is available for other configuration validation concepts: + +- [Single attribute validation](/terraform/plugin/framework/validation#attribute-validation) is a schema-based mechanism for implementing attribute-specific validation logic. +- [Type validation](/terraform/plugin/framework/validation#type-validation) is a schema-based mechanism for implementing reusable validation logic for any attribute using the type. + +-> Configuration validation in Terraform occurs without provider configuration ("offline"), therefore the ephemeral resource `Configure` method will not have been called. To implement validation with a configured API client, use logic within the `Open` method, which occurs during Terraform's planning phase when possible. + +## ConfigValidators Method + +The [`ephemeral.EphemeralResourceWithConfigValidators` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithConfigValidators) follows a similar pattern to attribute validation and allows for a more declarative approach. This enables consistent validation logic across multiple ephemeral resources. Each validator intended for this interface must implement the [`ephemeral.ConfigValidator` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#ConfigValidator). + +During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands, Terraform calls the provider `ValidateEphemeralResourceConfig` RPC, in which the framework calls the `ConfigValidators` method on ephemeral resources that implement the [`ephemeral.EphemeralResourceWithConfigValidators` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithConfigValidators). + +The [`terraform-plugin-framework-validators` Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) has a collection of common use case ephemeral resource configuration validators in the [`ephemeralvalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator). These use [path expressions](/terraform/plugin/framework/path-expressions) for matching attributes. + +This example will raise an error if a practitioner attempts to configure both `attribute_one` and `attribute_two`: + +```go +// Other methods to implement the ephemeral.EphemeralResource interface are omitted for brevity +type ThingEphemeralResource struct {} + +func (d ThingEphemeralResource) ConfigValidators(ctx context.Context) []ephemeral.ConfigValidator { + return []ephemeral.ConfigValidator{ + ephemeralvalidator.Conflicting( + path.MatchRoot("attribute_one"), + path.MatchRoot("attribute_two"), + ), + } +} +``` + +## ValidateConfig Method + +The [`ephemeral.EphemeralResourceWithValidateConfig` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithValidateConfig) is more imperative in design and is useful for validating unique functionality across multiple attributes that typically applies to a single ephemeral resource. + +During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands, Terraform calls the provider `ValidateEphemeralResourceConfig` RPC, in which the framework calls the `ValidateConfig` method on providers that implement the [`ephemeral.EphemeralResourceWithValidateConfig` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResourceWithValidateConfig). + +This example will raise a warning if a practitioner attempts to configure `attribute_one`, but not `attribute_two`: + +```go +// Other methods to implement the ephemeral.EphemeralResource interface are omitted for brevity +type ThingEphemeralResource struct {} + +type ThingEphemeralResourceModel struct { + AttributeOne types.String `tfsdk:"attribute_one"` + AttributeTwo types.String `tfsdk:"attribute_two"` +} + +func (d ThingEphemeralResource) ValidateConfig(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + var data ThingEphemeralResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // If attribute_one is not configured, return without warning. + if data.AttributeOne.IsNull() || data.AttributeOne.IsUnknown() { + return + } + + // If attribute_two is not null, return without warning. + if !data.AttributeTwo.IsNull() { + return + } + + resp.Diagnostics.AddAttributeWarning( + path.Root("attribute_two"), + "Missing Attribute Configuration", + "Expected attribute_two to be configured with attribute_one. "+ + "The ephemeral resource may return unexpected results.", + ) +} +``` diff --git a/website/docs/plugin/framework/functions/concepts.mdx b/website/docs/plugin/framework/functions/concepts.mdx index a933a4dfa..23835d778 100644 --- a/website/docs/plugin/framework/functions/concepts.mdx +++ b/website/docs/plugin/framework/functions/concepts.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Function Concepts' +page_title: Provider-defined functions description: >- - Terraform concepts for provider-defined functions. + Learn how provider-defined functions enable Terraform providers to define + functions for practitions to use in their Terraform configurations. --- -# Function Concepts +# 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 c7e7ec373..fd731ae0f 100644 --- a/website/docs/plugin/framework/functions/documentation.mdx +++ b/website/docs/plugin/framework/functions/documentation.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Document Functions' +page_title: Documenting functions description: >- - How to document provider-defined functions. + Learn how to document provider-defined functions with the Terraform plugin + framework. --- -# Document Functions +# 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 8ff7b1501..64dc74931 100644 --- a/website/docs/plugin/framework/functions/errors.mdx +++ b/website/docs/plugin/framework/functions/errors.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Function Errors' -description: |- - How to return function errors from the Terraform provider development - framework. +page_title: Returning errors from functions +description: >- + Learn how to return errors from provider-defined functions with the Terraform + plugin framework. --- -# Returning Function Errors +# Returning errors from function Providers use [`FuncError`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#FuncError) to surface a practitioner-facing error generated during execution of provider-defined functions. These errors are @@ -159,4 +159,4 @@ func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp resp.Error = function.ConcatFuncErrors(resp.Error, function.FuncErrorFromDiags(ctx, diags)) } -``` \ No newline at end of file +``` diff --git a/website/docs/plugin/framework/functions/implementation.mdx b/website/docs/plugin/framework/functions/implementation.mdx index 45c800878..c811870ee 100644 --- a/website/docs/plugin/framework/functions/implementation.mdx +++ b/website/docs/plugin/framework/functions/implementation.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Implement Functions' +page_title: Implement provider-defined functions description: >- - How to implement provider-defined functions in the provider development framework. + Learn how to implement provider-defined functions with the Terraform + plugin framework. --- -# Implement Functions +# 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 35126aa57..193c4ea1f 100644 --- a/website/docs/plugin/framework/functions/index.mdx +++ b/website/docs/plugin/framework/functions/index.mdx @@ -1,12 +1,13 @@ --- -page_title: 'Plugin Development - Framework: Functions' +page_title: Provider-defined functions overview description: >- - How to build functions in the provider development framework. Provider-defined - functions expose logic beyond Terraform's built-in functions and simplify - practitioner configurations. + Provider-defined functions expose logic beyond Terraform's built-in functions + that practitioners can use to simplify Terraform configurations. Learn how the + plugin framework can help you implement provider-defined functions. --- -# Functions + +# Provider-defined functions Functions are an abstraction that allow providers to expose computational logic beyond Terraform's [built-in functions](/terraform/language/functions) and simplify practitioner configurations. Provider-defined functions are supported in Terraform 1.8 and later. diff --git a/website/docs/plugin/framework/functions/parameters/bool.mdx b/website/docs/plugin/framework/functions/parameters/bool.mdx index 7e58f771f..3bf687562 100644 --- a/website/docs/plugin/framework/functions/parameters/bool.mdx +++ b/website/docs/plugin/framework/functions/parameters/bool.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Bool Function Parameter' +page_title: Boolean function parameters description: >- - Learn the bool function parameter type in the provider development framework. + Learn how to use the boolean function parameter type with the Terraform + plugin framework. --- -# Bool Function Parameter +# 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 a4e2f82ed..e088e5c55 100644 --- a/website/docs/plugin/framework/functions/parameters/dynamic.mdx +++ b/website/docs/plugin/framework/functions/parameters/dynamic.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Dynamic Function Parameter' +page_title: Dynamic function parameters description: >- - Learn the dynamic function parameter type in the provider development framework. + Learn how to use dynamic fynction paramters with the Terraform plugin + framework. --- -# Dynamic Function Parameter +# Dynamic function parameters diff --git a/website/docs/plugin/framework/functions/parameters/float32.mdx b/website/docs/plugin/framework/functions/parameters/float32.mdx index f41a8cb31..423df0cb2 100644 --- a/website/docs/plugin/framework/functions/parameters/float32.mdx +++ b/website/docs/plugin/framework/functions/parameters/float32.mdx @@ -1,7 +1,8 @@ --- -page_title: 'Plugin Development - Framework: Float32 Function Parameter' +page_title: Float32 function parameters description: >- - Learn the float32 function parameter type in the provider development framework. + Learn how to use the 32-bit floating point function parameter type with the + Terraform plugin framework. --- # Float32 Function Parameter diff --git a/website/docs/plugin/framework/functions/parameters/float64.mdx b/website/docs/plugin/framework/functions/parameters/float64.mdx index 11f97239f..7dbe213ac 100644 --- a/website/docs/plugin/framework/functions/parameters/float64.mdx +++ b/website/docs/plugin/framework/functions/parameters/float64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float64 Function Parameter' +page_title: Float64 function parameters description: >- - Learn the float64 function parameter type in the provider development framework. + Learn how to use the 64-bit floating point function parameter type with the + Terraform plugin framework. --- -# Float64 Function Parameter +# Float64 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/index.mdx b/website/docs/plugin/framework/functions/parameters/index.mdx index 65fb61e4f..76b4e6ed2 100644 --- a/website/docs/plugin/framework/functions/parameters/index.mdx +++ b/website/docs/plugin/framework/functions/parameters/index.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Function Parameters' +page_title: Function parameters description: >- - Learn the function parameter types in the provider development framework. - Parameters are positional data arguments in a function definition. + The Terraform plugin framework includes multiple built-in function parameter + types and supports dynamic parameters. Parameters are positional data + arguments in a function definition. --- -# Parameters +# 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. @@ -126,4 +127,4 @@ func (v CustomStringValue) ValidateParameter(ctx context.Context, req function.V } ``` -Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values \ No newline at end of file +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values diff --git a/website/docs/plugin/framework/functions/parameters/int32.mdx b/website/docs/plugin/framework/functions/parameters/int32.mdx index 9cb7c6885..de87e82b1 100644 --- a/website/docs/plugin/framework/functions/parameters/int32.mdx +++ b/website/docs/plugin/framework/functions/parameters/int32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int32 Function Parameter' +page_title: Int32 function parameters description: >- - Learn the int32 function parameter type in the provider development framework. + Learn how to use the 32-bit integer function parameter type with the + Terraform plugin framework. --- -# Int32 Function Parameter +# Int32 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/int64.mdx b/website/docs/plugin/framework/functions/parameters/int64.mdx index ab3b272d2..5e8411516 100644 --- a/website/docs/plugin/framework/functions/parameters/int64.mdx +++ b/website/docs/plugin/framework/functions/parameters/int64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int64 Function Parameter' +page_title: Int64 function parameters description: >- - Learn the int64 function parameter type in the provider development framework. + Learn how to use the 64-bit integer function parameter type with the + Terraform plugin framework. --- -# Int64 Function Parameter +# Int64 function parameters diff --git a/website/docs/plugin/framework/functions/parameters/list.mdx b/website/docs/plugin/framework/functions/parameters/list.mdx index 30da0e91a..edbca773a 100644 --- a/website/docs/plugin/framework/functions/parameters/list.mdx +++ b/website/docs/plugin/framework/functions/parameters/list.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: List Function Parameter' +page_title: List function parameters description: >- - Learn the list function parameter type in the provider development framework. + Learn how to use the list function parameter type with the + Terraform plugin framework. --- -# List Function Parameter +# 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 8eda096fa..49d35d893 100644 --- a/website/docs/plugin/framework/functions/parameters/map.mdx +++ b/website/docs/plugin/framework/functions/parameters/map.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Map Function Parameter' +page_title: Map function parameters description: >- - Learn the map function parameter type in the provider development framework. + Learn how to use the map function parameter type with the + Terraform plugin framework. --- -# Map Function Parameter +# 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 b5e30fb70..256d521ef 100644 --- a/website/docs/plugin/framework/functions/parameters/number.mdx +++ b/website/docs/plugin/framework/functions/parameters/number.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Number Function Parameter' +page_title: Number function parameters description: >- - Learn the number function parameter type in the provider development framework. + Learn how to use the arbitrary precision number function parameter type with + the Terraform plugin framework. --- -# Number Function Parameter +# Number function parameters diff --git a/website/docs/plugin/framework/functions/parameters/object.mdx b/website/docs/plugin/framework/functions/parameters/object.mdx index dd478f854..41bdcbae4 100644 --- a/website/docs/plugin/framework/functions/parameters/object.mdx +++ b/website/docs/plugin/framework/functions/parameters/object.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Object Function Parameter' +page_title: Object function parameters description: >- - Learn the object function parameter type in the provider development framework. + Learn how to use the object function parameter type with + the Terraform plugin framework. --- -# Object Function Parameter +# 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 ccd8117c5..dc53f9952 100644 --- a/website/docs/plugin/framework/functions/parameters/set.mdx +++ b/website/docs/plugin/framework/functions/parameters/set.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Set Function Parameter' +page_title: Set function parameters description: >- - Learn the set function parameter type in the provider development framework. + Learn how to use the set function parameter type with + the Terraform plugin framework. --- -# Set Function Parameter +# 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 c11e33b5c..bb449009c 100644 --- a/website/docs/plugin/framework/functions/parameters/string.mdx +++ b/website/docs/plugin/framework/functions/parameters/string.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: String Function Parameter' +page_title: String function parameters description: >- - Learn the string function parameter type in the provider development framework. + Learn how to use the string function parameter type with + the Terraform plugin framework. --- -# String Function Parameter +# 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 e2773b5ee..0da2af0d0 100644 --- a/website/docs/plugin/framework/functions/returns/bool.mdx +++ b/website/docs/plugin/framework/functions/returns/bool.mdx @@ -1,12 +1,13 @@ --- -page_title: 'Plugin Development - Framework: Bool Function Return' +page_title: Boolean return values description: >- - Learn the bool function return type in the provider development framework. + Learn how to use the boolean function return value type with the Terraform + plugin framework. --- -# Bool Function Return +# Boolean return values -Bool function return expects 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). +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). ## Function Definition diff --git a/website/docs/plugin/framework/functions/returns/dynamic.mdx b/website/docs/plugin/framework/functions/returns/dynamic.mdx index d9555abde..844d5926a 100644 --- a/website/docs/plugin/framework/functions/returns/dynamic.mdx +++ b/website/docs/plugin/framework/functions/returns/dynamic.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Dynamic Function Return' +page_title: Dynamic function return values description: >- - Learn the dynamic function return type in the provider development framework. + Learn how to use dynamic function return value types with the Terraform + plugin framework. --- -# Dynamic Function Return +# 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 b4688bb4b..fbd0f48e5 100644 --- a/website/docs/plugin/framework/functions/returns/float32.mdx +++ b/website/docs/plugin/framework/functions/returns/float32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float32 Function Return' +page_title: Float32 return values description: >- - Learn the float32 function return type in the provider development framework. + Learn how to use the 32-bit floating point function return value type with the + Terraform plugin framework. --- -# Float32 Function Return +# Float32 return values diff --git a/website/docs/plugin/framework/functions/returns/float64.mdx b/website/docs/plugin/framework/functions/returns/float64.mdx index edff1988a..2769c1d98 100644 --- a/website/docs/plugin/framework/functions/returns/float64.mdx +++ b/website/docs/plugin/framework/functions/returns/float64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float64 Function Return' +page_title: Float64 return values description: >- - Learn the float64 function return type in the provider development framework. + Learn how to use the 64-bit floating point function return value type with the + Terraform plugin framework. --- -# Float64 Function Return +# Float64 return values diff --git a/website/docs/plugin/framework/functions/returns/index.mdx b/website/docs/plugin/framework/functions/returns/index.mdx index 8a9dd6f19..a81c5bc83 100644 --- a/website/docs/plugin/framework/functions/returns/index.mdx +++ b/website/docs/plugin/framework/functions/returns/index.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Function Returns' +page_title: Function return values description: >- - Learn the function return types in the provider development framework. - A return describes the output data in a function definition. + The Terraform plugin framework includes multiple built-in function return + value types and supports dynamic return values. A return describes the output + data in a function definition. --- -# Returns +# 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 ff0e8f57a..b0c4b0c54 100644 --- a/website/docs/plugin/framework/functions/returns/int32.mdx +++ b/website/docs/plugin/framework/functions/returns/int32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int32 Function Return' +page_title: Int32 return values description: >- - Learn the int32 function return type in the provider development framework. + Learn how to use the 32-bit integer function return value type with the + Terraform plugin framework. --- -# Int32 Function Return +# Int32 return values diff --git a/website/docs/plugin/framework/functions/returns/int64.mdx b/website/docs/plugin/framework/functions/returns/int64.mdx index 25d06f532..a134eda66 100644 --- a/website/docs/plugin/framework/functions/returns/int64.mdx +++ b/website/docs/plugin/framework/functions/returns/int64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int64 Function Return' +page_title: Int64 return values description: >- - Learn the int64 function return type in the provider development framework. + Learn how to use the 64-bit integer function return value type with the + Terraform plugin framework. --- -# Int64 Function Return +# Int64 return values diff --git a/website/docs/plugin/framework/functions/returns/list.mdx b/website/docs/plugin/framework/functions/returns/list.mdx index 1a1e423c3..39cc953c7 100644 --- a/website/docs/plugin/framework/functions/returns/list.mdx +++ b/website/docs/plugin/framework/functions/returns/list.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: List Function Return' +page_title: List return values description: >- - Learn the list function return type in the provider development framework. + Learn how to use the list function return value type with the + Terraform plugin framework. --- -# List Function Return +# 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 71840f7fb..3ae20085a 100644 --- a/website/docs/plugin/framework/functions/returns/map.mdx +++ b/website/docs/plugin/framework/functions/returns/map.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Map Function Return' +page_title: Map return values description: >- - Learn the map function return type in the provider development framework. + Learn how to use the map function return value type with the + Terraform plugin framework. --- -# Map Function Return +# 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 2d5295109..74e6a2c01 100644 --- a/website/docs/plugin/framework/functions/returns/number.mdx +++ b/website/docs/plugin/framework/functions/returns/number.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Number Function Return' +page_title: Number return values description: >- - Learn the number function return type in the provider development framework. + Learn how to use the arbitrary precision number function return value type + with the Terraform plugin framework. --- -# Number Function Return +# Number return values diff --git a/website/docs/plugin/framework/functions/returns/object.mdx b/website/docs/plugin/framework/functions/returns/object.mdx index 59262f4b2..7f4a354a3 100644 --- a/website/docs/plugin/framework/functions/returns/object.mdx +++ b/website/docs/plugin/framework/functions/returns/object.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Object Function Return' +page_title: Object return values description: >- - Learn the object function return type in the provider development framework. + Learn how to use the object function return value type with the Terraform + plugin framework. --- -# Object Function Return +# 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 622fb44b3..1c32dc777 100644 --- a/website/docs/plugin/framework/functions/returns/set.mdx +++ b/website/docs/plugin/framework/functions/returns/set.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Set Function Return' +page_title: Set return values description: >- - Learn the set function return type in the provider development framework. + Learn how to use the set function return value type with the Terraform + plugin framework. --- -# Set Function Return +# 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 8daf2b2aa..d80f9fa5b 100644 --- a/website/docs/plugin/framework/functions/returns/string.mdx +++ b/website/docs/plugin/framework/functions/returns/string.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: String Function Return' +page_title: String return values description: >- - Learn the string function return type in the provider development framework. + Learn how to use the string function return value type with the Terraform + plugin framework. --- -# String Function Return +# 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 43d9b72b9..dad993547 100644 --- a/website/docs/plugin/framework/functions/testing.mdx +++ b/website/docs/plugin/framework/functions/testing.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Testing Functions' +page_title: Testing functions description: >- - How to test provider-defined functions. + Learn how to implement tests for provider-defined functions with the Terraform + plugin framework. --- -# Testing Functions +# 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 f0d02e3c0..8aecf5506 100644 --- a/website/docs/plugin/framework/getting-started/code-walkthrough.mdx +++ b/website/docs/plugin/framework/getting-started/code-walkthrough.mdx @@ -1,10 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Getting Started - Code Walkthrough' +page_title: Provider code walkthrough description: >- - How to setup and configure a simple plugin provider. + The Terraform plugin framework is an SDK that you can use to implement + Terraform providers. Learn how the framework can help you create a provider + by exploring its main components and libraries. --- -# Code Walkthrough +# 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 a2624da4d..a3d1777f4 100644 --- a/website/docs/plugin/framework/handling-data/accessing-values.mdx +++ b/website/docs/plugin/framework/handling-data/accessing-values.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Access State, Config, and Plan' -description: |- - How to read values from state, config, and plan in the Terraform plugin - framework. +page_title: Access state, configuration, and plan data +description: >- + Learn how to read values from Terraform's state, configuration, and plan with + the Terraform plugin framework. --- -# Accessing State, Config, and Plan +# Access state, configuration, and plan data There are various points at which the provider needs access to the data from the practitioner's configuration, Terraform's state, or generated plan. diff --git a/website/docs/plugin/framework/handling-data/attributes/bool.mdx b/website/docs/plugin/framework/handling-data/attributes/bool.mdx index 48ffb70a9..b1b7eb2e1 100644 --- a/website/docs/plugin/framework/handling-data/attributes/bool.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/bool.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Bool Attribute' +page_title: Boolean attributes description: >- - Learn the bool attribute type in the provider development framework. + Learn how to use boolean attributes with the Terraform plugin framework. --- -# Bool Attribute + +# Boolean attributes Bool attributes store a boolean true or false value. Values are represented by a [bool type](/terraform/plugin/framework/handling-data/types/bool) in the framework. @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a bool value to a [sche | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#BoolAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#BoolAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#BoolAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#BoolAttribute) | In this example, a resource schema defines a top level required bool attribute named `example_attribute`: @@ -103,6 +105,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 f399ce665..66da060bf 100644 --- a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Dynamic Attribute' +page_title: Dynamic attributes description: >- - Learn the dynamic attribute type in the provider development framework. + Learn how to use dynamic attributes with the Terraform plugin framework. --- -# Dynamic Attribute +# Dynamic attribute @@ -55,6 +55,7 @@ Use one of the following attribute types to directly add a dynamic value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#DynamicAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#DynamicAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#DynamicAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#DynamicAttribute) | In this example, a resource schema defines a top level required dynamic attribute named `example_attribute`: @@ -133,6 +134,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 0d794db90..f0a654718 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float32 Attribute' +page_title: Float32 attributes description: >- - Learn the float32 attribute type in the provider development framework. + Learn how to use 32-bit floating point attributes with the Terraform plugin + framework. --- -# Float32 Attribute +# Float32 attributes @@ -31,6 +32,7 @@ Use one of the following attribute types to directly add a float32 value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float32Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float32Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float32Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Float32Attribute) | In this example, a resource schema defines a top level required float32 attribute named `example_attribute`: @@ -109,6 +111,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 b8364f462..a43e8b7ab 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float64 Attribute' +page_title: Float64 attributes description: >- - Learn the float64 attribute type in the provider development framework. + Learn how to use 64-bit floating point attributes with the Terraform plugin + framework. --- -# Float64 Attribute +# Float64 attributes @@ -31,6 +32,7 @@ Use one of the following attribute types to directly add a float64 value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float64Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float64Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float64Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Float64Attribute) | In this example, a resource schema defines a top level required float64 attribute named `example_attribute`: @@ -109,6 +111,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 0424c8588..58397eacb 100644 --- a/website/docs/plugin/framework/handling-data/attributes/index.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/index.mdx @@ -1,8 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Attributes' +page_title: Attributes description: >- - Learn the attribute types in the provider development framework. Attributes - are fields in a resource, data source, or provider schema. + The Terraform plugin framework includes multiple built-in attribute types + and supports custom and dynamic attribute types. Each attribute and block in a + Terraform resource, data source, or provider schema maps to a framework or + custom type. --- # Attributes diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx index ad4b46a8f..cd9c443e9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int32 Attribute' +page_title: Int32 attributes description: >- - Learn the int32 attribute type in the provider development framework. + Learn how to use 32-bit integer attributes with the Terraform plugin + framework. --- -# Int32 Attribute +# Int32 attributes @@ -31,6 +32,7 @@ Use one of the following attribute types to directly add a int32 value to a [sch | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int32Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int32Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int32Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Int32Attribute) | In this example, a resource schema defines a top level required int32 attribute named `example_attribute`: @@ -109,6 +111,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 5d793a9cf..6bba8efe4 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int64 Attribute' +page_title: Int64 attributes description: >- - Learn the int64 attribute type in the provider development framework. + Learn how to use 64-bit integer attributes with the Terraform plugin + framework. --- -# Int64 Attribute +# Int64 attributes @@ -31,6 +32,7 @@ Use one of the following attribute types to directly add a int64 value to a [sch | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int64Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int64Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int64Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Int64Attribute) | In this example, a resource schema defines a top level required int64 attribute named `example_attribute`: @@ -109,6 +111,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 6c4258850..1a3bd86e6 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: List Nested Attribute' +page_title: List nested attributes description: >- - Learn the list nested attribute type in the provider development framework. + Learn how to use list nested attributes with the Terraform plugin framework. --- -# List Nested Attribute +# 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). @@ -32,6 +32,7 @@ Use one of the following attribute types to directly add a list nested value to | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ListNestedAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ListNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListNestedAttribute) | The `NestedObject` field must be defined, which represents the [object value type](/terraform/plugin/framework/handling-data/types/object) of every element of the list. @@ -158,6 +159,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 159966f9b..50ffcf6b5 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: List Attribute' +page_title: List attributes description: >- - Learn the list attribute type in the provider development framework. + Learn how to use list attributes with the Terraform plugin framework. --- -# List Attribute +# 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. @@ -25,6 +25,7 @@ Use one of the following attribute types to directly add a list value to a [sche | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ListAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ListAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListAttribute) | The `ElementType` field must be defined, which represents the single [value type](/terraform/plugin/framework/handling-data/types) of every element of the list. @@ -127,6 +128,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 63f816eeb..53424755a 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Map Nested Attribute' +page_title: Map nested attributes description: >- - Learn the map nested attribute type in the provider development framework. + Learn how to use map nested attributes with the Terraform plugin framework. --- # Map Nested Attribute @@ -32,6 +32,7 @@ Use one of the following attribute types to directly add a map nested value to a | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#MapNestedAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#MapNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#MapNestedAttribute) | The `NestedObject` field must be defined, which represents the [object value type](/terraform/plugin/framework/handling-data/types/object) of every element of the list. @@ -158,6 +159,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 71a52b24b..a5b3faea9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Map Attribute' +page_title: Map attributes description: >- - Learn the map attribute type in the provider development framework. + Learn how to use map attributes with the Terraform plugin framework. --- -# Map Attribute +# 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. @@ -28,6 +28,7 @@ Use one of the following attribute types to directly add a map value to a [schem | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#MapAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#MapAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#MapAttribute) | The `ElementType` field must be defined, which represents the single [value type](/terraform/plugin/framework/handling-data/types) of every element of the map. @@ -130,6 +131,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 3f47e5d5a..270a35070 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Number Attribute' +page_title: Number attributes description: >- - Learn the number attribute type in the provider development framework. + Learn how to use arbitrary precision number attributes with the Terraform + plugin framework. --- -# Number Attribute +# Number attributes @@ -31,6 +32,7 @@ Use one of the following attribute types to directly add a number value to a [sc | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#NumberAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#NumberAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#NumberAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#NumberAttribute) | In this example, a resource schema defines a top level required number attribute named `example_attribute`: @@ -109,6 +111,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 4af6dda0d..de7b56083 100644 --- a/website/docs/plugin/framework/handling-data/attributes/object.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/object.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Object Attribute' +page_title: Object attributes description: >- - Learn the object attribute type in the provider development framework. + Learn how to use object attributes with the Terraform plugin framework. --- -# Object Attribute +# Object attributes @@ -34,6 +34,7 @@ Use one of the following attribute types to directly add a map value to a [schem | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ObjectAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ObjectAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ObjectAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ObjectAttribute) | The `AttributeTypes` field must be defined, which represents the mapping of explicit string object attribute names to [value types](/terraform/plugin/framework/handling-data/types). @@ -164,6 +165,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 04bc7388a..01ce7e51f 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Set Nested Attribute' +page_title: Set nested attributes description: >- - Learn the set nested attribute type in the provider development framework. + Learn how to use set nested attributes with the Terraform plugin framework. --- -# Set Nested Attribute +# 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). @@ -32,6 +32,7 @@ Use one of the following attribute types to directly add a set nested value to a | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SetNestedAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SetNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetNestedAttribute) | The `NestedObject` field must be defined, which represents the [object value type](/terraform/plugin/framework/handling-data/types/object) of every element of the set. diff --git a/website/docs/plugin/framework/handling-data/attributes/set.mdx b/website/docs/plugin/framework/handling-data/attributes/set.mdx index e75db4abc..512489aad 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Set Attribute' +page_title: Set attributes description: >- - Learn the set attribute type in the provider development framework. + Learn how to use set attributes with the Terraform plugin framework. --- -# Set Attribute +# 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. @@ -25,6 +25,7 @@ Use one of the following attribute types to directly add a set value to a [schem | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SetAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SetAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetAttribute) | The `ElementType` field must be defined, which represents the single [value type](/terraform/plugin/framework/handling-data/types) of every element of the set. 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 eea6ac4f0..24e52a883 100644 --- a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Single Nested Attribute' +page_title: Single nested attributes description: >- - Learn the single nested attribute type in the provider development framework. + Learn how to use single nested attributes with the Terraform plugin framework. --- -# Single Nested Attribute +# 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. @@ -28,6 +28,7 @@ Use one of the following attribute types to directly add a single nested value t | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SingleNestedAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SingleNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SingleNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SingleNestedAttribute) | In most use cases, the `Attributes` field should be defined, which represents the mapping of explicit string attribute names to nested attributes. @@ -154,6 +155,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 cb02da57d..6d414dbff 100644 --- a/website/docs/plugin/framework/handling-data/attributes/string.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/string.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: String Attribute' +page_title: String attributes description: >- - Learn the string attribute type in the provider development framework. + Learn how to use string attributes with the Terraform plugin framework. --- -# String Attribute +# 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. @@ -25,6 +25,7 @@ Use one of the following attribute types to directly add a string value to a [sc | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#StringAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#StringAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#StringAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#StringAttribute) | In this example, a resource schema defines a top level required string attribute named `example_attribute`: @@ -111,6 +112,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 f9b37d1d8..af96c5021 100644 --- a/website/docs/plugin/framework/handling-data/blocks/index.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/index.mdx @@ -1,8 +1,8 @@ --- -page_title: 'Plugin Development - Framework: Blocks' +page_title: Blocks description: >- - Learn the block types in the provider development framework. Blocks - are containers for nested attributes and blocks in a resource, data source, or + Learn how to use block types with the Terraform plugin framework. Blocks are + containers for nested attributes and blocks in a resource, data source, or provider schema. --- 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 06fe2fc98..e17c362f4 100644 --- a/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: List Nested Block' +page_title: List nested blocks description: >- - Learn the list nested block type in the provider development framework. + Learn how to implement the list nested block type with the Terraform plugin + framework. --- -# List Nested Block +# List nested blocks @@ -45,6 +46,7 @@ Use one of the following block types to directly add a list nested value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ListNestedBlock) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ListNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListNestedBlock) | The `NestedObject` field must be defined, which represents the [object value type](/terraform/plugin/framework/handling-data/types/object) of every element of the list. 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 937dd296a..e780d5b5c 100644 --- a/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Set Nested Block' +page_title: Set nested blocks description: >- - Learn the set nested block type in the provider development framework. + Learn to implement the set nested block type with the Terraform plugin framework. --- -# Set Nested Block +# Set nested blocks @@ -45,6 +45,7 @@ Use one of the following block types to directly add a list nested value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SetNestedBlock) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SetNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetNestedBlock) | The `NestedObject` field must be defined, which represents the [object value type](/terraform/plugin/framework/handling-data/types/object) of every element of the set. 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 63b80e941..e078b1103 100644 --- a/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Single Nested Block' +page_title: Single nested blocks description: >- - Learn the single nested block type in the provider development framework. + Learn to implement the single nested block type with the Terraform plugin + framework. --- -# Single Nested Block +# Single nested blocks @@ -40,6 +41,7 @@ Use one of the following block types to directly add a single nested value to a | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SingleNestedBlock) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SingleNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SingleNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SingleNestedBlock) | In most use cases, the `Attributes` or `Blocks` field should be defined, which represents the mapping of explicit string attribute names to nested attributes and/or blocks. diff --git a/website/docs/plugin/framework/handling-data/dynamic-data.mdx b/website/docs/plugin/framework/handling-data/dynamic-data.mdx index 0f0ce2064..07f845f41 100644 --- a/website/docs/plugin/framework/handling-data/dynamic-data.mdx +++ b/website/docs/plugin/framework/handling-data/dynamic-data.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Handling Data - Dynamic Data' +page_title: Handling dynamic data description: >- - How to handle data when utilizing dynamic types. + Learn how to handle data when using dynamic types in the Terraform plugin + framework. --- -# Dynamic Data +# 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 54b066a23..80cca2a1d 100644 --- a/website/docs/plugin/framework/handling-data/path-expressions.mdx +++ b/website/docs/plugin/framework/handling-data/path-expressions.mdx @@ -1,12 +1,13 @@ --- -page_title: 'Plugin Development - Framework: Path Expressions' +page_title: Path expressions description: >- - How to implement path expressions in the provider development framework. + Learn how to implement path expressions in the Terraform plugin framework. Path expressions are logic built on top of paths, which may represent one or more actual paths within schema data. --- -# Path Expressions + +# Path expressions Path expressions are logic built on top of [paths](/terraform/plugin/framework/paths), which may represent one or more actual paths within a schema or schema-based data. Expressions enable providers to work outside the restrictions of absolute paths and steps. diff --git a/website/docs/plugin/framework/handling-data/paths.mdx b/website/docs/plugin/framework/handling-data/paths.mdx index f08cbec1f..1df50cf70 100644 --- a/website/docs/plugin/framework/handling-data/paths.mdx +++ b/website/docs/plugin/framework/handling-data/paths.mdx @@ -1,8 +1,8 @@ --- -page_title: 'Plugin Development - Framework: Paths' +page_title: Paths description: >- - How to implement paths in the provider development framework. - Paths represent a location within a schema or schema-based data. + Learn how to implement paths in the Terraform plugin framework. Paths + represent a location within a schema or schema-based data. --- # Paths diff --git a/website/docs/plugin/framework/handling-data/schemas.mdx b/website/docs/plugin/framework/handling-data/schemas.mdx index a36661221..e406565e5 100644 --- a/website/docs/plugin/framework/handling-data/schemas.mdx +++ b/website/docs/plugin/framework/handling-data/schemas.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Schemas' +page_title: Schemas description: >- - How to define a schema using the provider development framework. Schemas + Learn how to define a schema using the Terraform plugin framework. Schemas specify the constraints of Terraform configuration blocks. --- @@ -17,8 +17,9 @@ Each concept has its own `schema` package and `Schema` type, which defines funct - [Providers](/terraform/plugin/framework/providers): [`provider/schema.Schema`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Schema) - [Resources](/terraform/plugin/framework/resources): [`resource/schema.Schema`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Schema) - [Data Sources](/terraform/plugin/framework/data-sources): [`datasource/schema.Schema`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Schema) +- [Ephemeral Resources](/terraform/plugin/framework/ephemeral-resources): [`ephemeral/schema.Schema`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Schema) -During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands, Terraform calls the provider [`GetProviderSchema`](/terraform/plugin/framework/internals/rpcs#getproviderschema-rpc) RPC, in which the framework calls the [`provider.Provider` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Schema), and the [`resource.Resource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Schema) and [`datasource.DataSource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource#DataSource.Schema) on each of the resources and data sources, respectively. +During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan) and [`terraform apply`](/terraform/cli/commands/apply) commands, Terraform calls the provider [`GetProviderSchema`](/terraform/plugin/framework/internals/rpcs#getproviderschema-rpc) RPC, in which the framework calls the [`provider.Provider` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#Provider.Schema), the [`resource.Resource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#Resource.Schema), [`datasource.DataSource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource#DataSource.Schema), and the [`ephemeral.EphemeralResource` interface `Schema` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#EphemeralResource.Schema) on each of the resource types, respectively. ## Version @@ -31,7 +32,7 @@ provider or data source schemas and can be omitted. ## DeprecationMessage -Not every resource, data source, or provider will be supported forever. +Not every resource, data source, ephemeral resource, or provider will be supported forever. Sometimes designs change or APIs are deprecated. Schemas that have their `DeprecationMessage` property set will display that message as a warning when that provider, data source, or resource is used. A good message will tell diff --git a/website/docs/plugin/framework/handling-data/terraform-concepts.mdx b/website/docs/plugin/framework/handling-data/terraform-concepts.mdx index 0272cbb47..d8602e8a3 100644 --- a/website/docs/plugin/framework/handling-data/terraform-concepts.mdx +++ b/website/docs/plugin/framework/handling-data/terraform-concepts.mdx @@ -1,16 +1,17 @@ --- -page_title: 'Plugin Development - Framework: Handling Data - Terraform Concepts' +page_title: Terraform data concepts description: >- - Configuration, Schemas, Attributes and Blocks. + Learn how the Terraform plugin framework handles data by mapping Terraform + configuration to schemas, attributes, and blocks. --- -# Terraform Concepts +# 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. ## Schemas -Schemas specify the data structure and types of a provider, resource, or data source that is exposed to Terraform. This includes the configuration written by practitioners, any planning data, and the state stored by Terraform which can be referenced in other configuration. Providers, resources, and data sources have their own concept-specific types and available functionality. +Schemas specify the data structure and types of a provider, resource, data source, or ephemeral resource that is exposed to Terraform. This includes the configuration written by practitioners, any planning data, and the state stored by Terraform which can be referenced in other configuration. Providers, resources, data sources, and ephemeral resources have their own concept-specific types and available functionality. Each part of the data within a schema is defined as either an attribute or block. In general, attributes set values and blocks are containers for other attributes and blocks. Each have differing configuration syntax and behaviors. @@ -62,7 +63,7 @@ In Terraform operations where the plan data is available to providers, the frame -Only managed resources and data resources implement this data concept. +Only managed resources and data sources implement this data concept. diff --git a/website/docs/plugin/framework/handling-data/types/bool.mdx b/website/docs/plugin/framework/handling-data/types/bool.mdx index aed82e4e9..d771afd10 100644 --- a/website/docs/plugin/framework/handling-data/types/bool.mdx +++ b/website/docs/plugin/framework/handling-data/types/bool.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Bool Type' +page_title: Boolean types description: >- - Learn the bool value type in the provider development framework. + Learn how to implement boolean value types with the Terraform plugin framework. --- -# Bool Type +# Bool types Bool types store a boolean true or false value. @@ -19,6 +19,7 @@ Use one of the following attribute types to directly add a bool value to a [sche | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#BoolAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#BoolAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#BoolAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.BoolAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#BoolAttribute) | If the bool value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.BoolType` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/custom.mdx b/website/docs/plugin/framework/handling-data/types/custom.mdx index 9632d9bb5..ad47f3372 100644 --- a/website/docs/plugin/framework/handling-data/types/custom.mdx +++ b/website/docs/plugin/framework/handling-data/types/custom.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Handling Data - Custom Types' +page_title: Custom types description: >- - Custom Types. + Learn how to implement custom types with the Terraform plugin framework. --- -# Custom Types +# 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 f647f3789..cd0194eef 100644 --- a/website/docs/plugin/framework/handling-data/types/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/types/dynamic.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Dynamic Type' +page_title: Dynamic types description: >- - Learn the dynamic value type in the provider development framework. + Learn how to implement dynamic types with the Terraform plugin framework. --- -# Dynamic Type +# Dynamic types @@ -29,6 +29,7 @@ Use one of the following attribute types to directly add a dynamic value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#DynamicAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#DynamicAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#DynamicAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.DynamicAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#DynamicAttribute) | Dynamic values are not supported as the element type of a [collection type](/terraform/plugin/framework/handling-data/types#collection-types) or within [collection attribute types](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types). diff --git a/website/docs/plugin/framework/handling-data/types/float32.mdx b/website/docs/plugin/framework/handling-data/types/float32.mdx index abc351d3a..bc3bdc8e2 100644 --- a/website/docs/plugin/framework/handling-data/types/float32.mdx +++ b/website/docs/plugin/framework/handling-data/types/float32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float32 Type' +page_title: Float32 types description: >- - Learn the float32 value type in the provider development framework. + Learn how to implement 32-bit floating point value types with the Terraform + plugin framework. --- -# Float32 Type +# Float32 types @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a float32 value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float32Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float32Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float32Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Float32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Float32Attribute) | If the float32 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.Float32Type` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/float64.mdx b/website/docs/plugin/framework/handling-data/types/float64.mdx index 1e6fca4e2..6fc389679 100644 --- a/website/docs/plugin/framework/handling-data/types/float64.mdx +++ b/website/docs/plugin/framework/handling-data/types/float64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Float64 Type' +page_title: Float64 types description: >- - Learn the float64 value type in the provider development framework. + Learn how to implement 64-bit floating point value types with the Terraform plugin + framework. --- -# Float64 Type +# Float64 types @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a float64 value to a [s | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Float64Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Float64Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Float64Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Float64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Float64Attribute) | If the float64 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.Float64Type` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/index.mdx b/website/docs/plugin/framework/handling-data/types/index.mdx index 8f0b3ead7..4e5f9103a 100644 --- a/website/docs/plugin/framework/handling-data/types/index.mdx +++ b/website/docs/plugin/framework/handling-data/types/index.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Types' +page_title: Data types description: >- - Learn the types in the provider development framework. Attributes and blocks - in a resource, data source, or provider schema map to specific framework types. + The Terraform plugin framework includes multiple built-in attribute types + and supports custom and dynamic attribute types. You can implement custom + types based off of the built-in attribute types. --- -# Types +# 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 1a26debc4..4cc69ca3f 100644 --- a/website/docs/plugin/framework/handling-data/types/int32.mdx +++ b/website/docs/plugin/framework/handling-data/types/int32.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int32 Type' +page_title: Int32 types description: >- - Learn the int32 value type in the provider development framework. + Learn how to implement 32-bit integer value types with the Terraform plugin + framework. --- -# Int32 Type +# Int32 types @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a int32 value to a [sch | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int32Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int32Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int32Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Int32Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Int32Attribute) | If the int32 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.Int32Type` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/int64.mdx b/website/docs/plugin/framework/handling-data/types/int64.mdx index 252ec84da..5d4f99da8 100644 --- a/website/docs/plugin/framework/handling-data/types/int64.mdx +++ b/website/docs/plugin/framework/handling-data/types/int64.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Int64 Type' +page_title: Int64 types description: >- - Learn the int64 value type in the provider development framework. + Learn how to implement 64-bit integer value types with the Terraform plugin + framework. --- -# Int64 Type +# Int64 types @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a int64 value to a [sch | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#Int64Attribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#Int64Attribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#Int64Attribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.Int64Attribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#Int64Attribute) | If the int64 value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.Int64Type` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/list.mdx b/website/docs/plugin/framework/handling-data/types/list.mdx index f13db98ee..252f1df5e 100644 --- a/website/docs/plugin/framework/handling-data/types/list.mdx +++ b/website/docs/plugin/framework/handling-data/types/list.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: List Type' +page_title: List types description: >- - Learn the list value type in the provider development framework. + Learn how to implement list value types with the Terraform pluginprovider + framework. --- -# List Type +# List types List types store an ordered collection of single element type. @@ -19,6 +20,7 @@ Use one of the following attribute types to directly add a list of a single elem | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ListAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ListAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListAttribute) | Use one of the following attribute types to directly add a list of a nested attributes to a [schema](/terraform/plugin/framework/handling-data/schemas) or [nested attribute type](/terraform/plugin/framework/handling-data/attributes#nested-attribute-types): @@ -30,6 +32,8 @@ Use one of the following attribute types to directly add a list of a nested attr | [Provider](/terraform/plugin/framework/provider) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ListNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ListNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ListNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ListNestedBlock) | If the list value should be the element type of another [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElementType` field to `types.ListType{ElemType: /* ... */}` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/map.mdx b/website/docs/plugin/framework/handling-data/types/map.mdx index 96f3f0b78..b6be68b9d 100644 --- a/website/docs/plugin/framework/handling-data/types/map.mdx +++ b/website/docs/plugin/framework/handling-data/types/map.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Map Type' +page_title: Map types description: >- - Learn the map value type in the provider development framework. + Learn how to implement mapping value types with the Terraform plugin + framework. --- -# Map Type +# Map type Map types store an ordered collection of single element type. @@ -19,6 +20,7 @@ Use one of the following attribute types to directly add a map of a single eleme | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#MapAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#MapAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.MapAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#MapAttribute) | Use one of the following attribute types to directly add a map of a nested attributes to a [schema](/terraform/plugin/framework/handling-data/schemas) or [nested attribute type](/terraform/plugin/framework/handling-data/attributes#nested-attribute-types): @@ -26,7 +28,8 @@ Use one of the following attribute types to directly add a map of a nested attri |-------------|----------------| | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#MapNestedAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#MapNestedAttribute) | -| [Resource](/terraform/plugin/framework/resources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetNestedAttribute) | +| [Resource](/terraform/plugin/framework/resources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#MapNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.MapNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#MapNestedAttribute) | If the map value should be the element type of another [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElementType` field to `types.MapType{ElemType: /* ... */}` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/number.mdx b/website/docs/plugin/framework/handling-data/types/number.mdx index b8312efbd..99c7315db 100644 --- a/website/docs/plugin/framework/handling-data/types/number.mdx +++ b/website/docs/plugin/framework/handling-data/types/number.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Number Type' +page_title: Number types description: >- - Learn the float64 value type in the provider development framework. + Learn how to implement arbitrary precision number value types with the Terraform plugin + framework. --- -# Number Type +# Number types @@ -25,6 +26,7 @@ Use one of the following attribute types to directly add a number value to a [sc | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#NumberAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#NumberAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#NumberAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.NumberAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#NumberAttribute) | If the number value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.NumberType` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/object.mdx b/website/docs/plugin/framework/handling-data/types/object.mdx index 3c62b2e0b..b6ae89545 100644 --- a/website/docs/plugin/framework/handling-data/types/object.mdx +++ b/website/docs/plugin/framework/handling-data/types/object.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Object Type' +page_title: Object types description: >- - Learn the object value type in the provider development framework. + Learn how to implement object value types with the Terraform plugin framework. --- -# Object Type +# 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. @@ -28,6 +28,8 @@ Use one of the following attribute types to directly add a single structure of a | [Provider](/terraform/plugin/framework/provider) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SingleNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SingleNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SingleNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SingleNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SingleNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SingleNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SingleNestedBlock) | If a wrapping collection is needed on the structure of nested attributes, any of the other nested attribute and nested block types can be used. @@ -38,6 +40,7 @@ Use one of the following attribute types to directly add an object value directl | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#ObjectAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#ObjectAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#ObjectAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.ObjectAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#ObjectAttribute) | If the object value should be the element type of another [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElementType` field to `types.ObjectType{AttrTypes: /* ... */}` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/set.mdx b/website/docs/plugin/framework/handling-data/types/set.mdx index d218b1224..73e0576cb 100644 --- a/website/docs/plugin/framework/handling-data/types/set.mdx +++ b/website/docs/plugin/framework/handling-data/types/set.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Set Type' +page_title: Set types description: >- - Learn the set value type in the provider development framework. + Learn how to implement set value types with the Terraform plugin framework. --- -# Set Type +# Set types Set types store an ordered collection of single element type. @@ -19,6 +19,7 @@ Use one of the following attribute types to directly add a set of a single eleme | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#SetAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SetAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetAttribute) | Use one of the following attribute types to directly add a set of a nested attributes to a [schema](/terraform/plugin/framework/handling-data/schemas) or [nested attribute type](/terraform/plugin/framework/handling-data/attributes#nested-attribute-types): @@ -30,6 +31,8 @@ Use one of the following attribute types to directly add a set of a nested attri | [Provider](/terraform/plugin/framework/provider) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#SetNestedBlock) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetNestedAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#SetNestedBlock) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetNestedAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetNestedAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.SetNestedBlock`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#SetNestedBlock) | If the set value should be the element type of another [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElementType` field to `types.SetType{ElemType: /* ... */}` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/string.mdx b/website/docs/plugin/framework/handling-data/types/string.mdx index 0284d5497..c4644153d 100644 --- a/website/docs/plugin/framework/handling-data/types/string.mdx +++ b/website/docs/plugin/framework/handling-data/types/string.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: String Type' +page_title: String types description: >- - Learn the string value type in the provider development framework. + Learn how to implement string value types with the Terraform plugin framework. --- -# String Type +# String types String types store a collection of UTF-8 encoded bytes. @@ -19,6 +19,7 @@ Use one of the following attribute types to directly add a string value to a [sc | [Data Source](/terraform/plugin/framework/data-sources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/datasource/schema#StringAttribute) | | [Provider](/terraform/plugin/framework/provider) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider/schema#StringAttribute) | | [Resource](/terraform/plugin/framework/resources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema#StringAttribute) | +| [Ephemeral Resource](/terraform/plugin/framework/ephemeral-resources) | [`schema.StringAttribute`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral/schema#StringAttribute) | If the string value should be the element type of a [collection attribute type](/terraform/plugin/framework/handling-data/attributes#collection-attribute-types), set the `ElemType` field to `types.StringType` or the appropriate [custom type](#extending). diff --git a/website/docs/plugin/framework/handling-data/types/tuple.mdx b/website/docs/plugin/framework/handling-data/types/tuple.mdx index c05b603ca..1276559a9 100644 --- a/website/docs/plugin/framework/handling-data/types/tuple.mdx +++ b/website/docs/plugin/framework/handling-data/types/tuple.mdx @@ -1,24 +1,24 @@ --- -page_title: 'Plugin Development - Framework: Tuple Type' +page_title: Tuple types description: >- - Learn the tuple value type in the provider development framework. + Learn how to implement tuple value types with the Terraform plugin framework. --- +# Tuple types + The tuple type doesn't have associated schema attributes as it has limited real world application. Provider developers will only encounter tuples when handling provider-defined function variadic parameters or dynamic values. -# Tuple Type - Tuple types store an ordered collection of elements where each element has it's own type. Values must have **exactly** the same number of elements (no more and no fewer), and the value in each position must match the specified type for that position. The tuple type is used to express Terraform's [tuple type constraint](/terraform/language/expressions/type-constraints#tuple). ## Schema Definitions -The tuple type is not supported in schema definitions of provider, data sources, or managed resources as it has limited real world application. +The tuple type is not supported in schema definitions of provider, data sources, ephemeral resources, or managed resources as it has limited real world application. ## Accessing Values diff --git a/website/docs/plugin/framework/handling-data/writing-state.mdx b/website/docs/plugin/framework/handling-data/writing-state.mdx index 6636fbcb5..200ef6501 100644 --- a/website/docs/plugin/framework/handling-data/writing-state.mdx +++ b/website/docs/plugin/framework/handling-data/writing-state.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Writing State' +page_title: Writing state description: >- - How to write and update the Terraform statefile using the provider development - framework. + Learn how to write and update the Terraform statefile with the Terraform + plugin framework. --- -# Writing State +# Writing state One of the primary jobs of a Terraform provider is to manage the provider's resources and data sources in the [Terraform state](/terraform/language/state). Writing values to state diff --git a/website/docs/plugin/framework/index.mdx b/website/docs/plugin/framework/index.mdx index 1c07c50ce..59379ac0c 100644 --- a/website/docs/plugin/framework/index.mdx +++ b/website/docs/plugin/framework/index.mdx @@ -1,10 +1,11 @@ --- -page_title: "Home - Plugin Development: Framework" -description: |- - Develop Terraform providers using the recommended plugin framework. +page_title: Terraform plugin framework +description: >- + The Terraform plugin framework is an SDK that you can use to develop Terraform + providers. Learn how the plugin framework works with Terraform core. --- -# Terraform Plugin Framework +# 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 64987052c..3212e2b71 100644 --- a/website/docs/plugin/framework/internals/index.mdx +++ b/website/docs/plugin/framework/internals/index.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Internals' +page_title: Framework internals description: >- - Framewwork internals. + The Terraform plugin framework is a set of libraries implemented in Go. + Learn about the internal implementation details of the framework. --- -# Internals +# 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 d09efe41a..4e12a18fe 100644 --- a/website/docs/plugin/framework/internals/rpcs.mdx +++ b/website/docs/plugin/framework/internals/rpcs.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: RPCs' +page_title: Framework RPCs description: >- - Relationship between RPCs and framework functionality. + Learn how Terraform uses RPCs to support provider functionality. --- -# RPCs and Framework Functionality +# 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 745896a3e..4042f31d2 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/attribute-schema.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Attribute Schema: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute schema description: >- - Migrate attributes from SDKv2 to the plugin Framework + Learn how to iteratively migrate from the SDKv2 to the plugin framework using + the terraform-plugin-mux Go library. --- -# Attribute Schema +# Migrating attribute schema Attributes define how users can configure values for your Terraform provider, resources, and data sources. Refer to [Schemas - Attributes](/terraform/plugin/framework/handling-data/schemas#attributes) in the Framework documentation for details. 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 bcbc2de25..1f4ee4bcc 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/blocks-computed.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Computed Blocks: Migrating from SDKv2 to the Framework' +page_title: Migrating computed blocks description: >- - Migrate blocks with computed fields from SDKv2 to attribute validators in the plugin Framework. + Learn how to igrate blocks with computed fields from SDKv2 to attribute + validators in the plugin framework. --- -# Blocks with Computed Fields +# Migrating blocks with computed fields Some providers, resources, and data sources include repeatable nested blocks in their attributes. Some blocks contain fields with `Computed: true`, which means that the provider code can define the value or that it could come from the diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx index 1691d9cec..353029dee 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/blocks.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Blocks: Migrating from SDKv2 to the Framework' +page_title: Migrating blocks description: >- - Migrate blocks from SDKv2 to attribute validators in the plugin Framework. + Learn how to migrate blocks from SDKv2 to attribute validators in the plugin + framework. --- -# Blocks +# Migrating blocks Some providers, resources, and data sources include repeatable nested blocks in their attributes. These nested blocks typically represent separate objects that are related to (or embedded within) the containing object. 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 40538fd0f..f358d8d62 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/default-values.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Attribute default values: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute default values description: >- - Specify a default when the Terraform configuration does not supply a value for resource attributes. - Migrate attribute defaults in SDKv2 to AttributePlanModifier in the Framework. + Learn how to migrate attribute default values from SDKv2 by using an + attribute plan modifier in the plugin framework. --- -# Default Values +# 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 8a594ca86..6efe29b7a 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/fields.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Attribute Fields: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute fields description: >- - Migrate attribute required, optional, computed, and sensitive fields from SDKv2 to the plugin Framework + Learn how to migrate attribute required, optional, computed, and sensitive + fields from SDKv2 to the plugin framework. --- -# Attribute Fields +# Migrating attribute fields A subset of attribute fields, such as required, optional, computed, or sensitive, define attribute behavior as boolean flags. Refer to [Schemas - Attributes](/terraform/plugin/framework/handling-data/schemas#required) in the Framework documentation for details. 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 a5992eda6..1a119eff6 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/force-new.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Attribute ForceNew triggers: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute ForceNew triggers description: >- - Migrate attribute force new in SDKv2 to an attribute plan modifier in the Framework. + Learn how to migrate attribute ForceNew triggers in SDKv2 to attribute plan + modifiers in the framework. --- -# ForceNew +# Migrating attribute ForceNew triggers In Terraform, sometimes a resource must be replaced when the value of an attribute changes. In SDKv2, this is accomplished via the `ForceNew` field. In the Framework, you implement the same behavior via a `RequiresReplace` plan diff --git a/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx b/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx index cab4437a1..86d633fa7 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/types.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Attribute Types: Migrating from SDKv2 to the Framework' +page_title: Migrating atrribute types description: >- - Migrate attribute type from SDKv2 to the plugin Framework + Learn how to migrate attribute type from SDKv2 to the plugin Framework. --- -# Attribute Types +# Migrating attribute types An attribute either contains a primitive type, such as an integer or a string, or contains other attributes. Attributes that contain other attributes are referred to as nested attributes. Refer to 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 25082e857..8c33f4c01 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/validators-custom.mdx @@ -1,10 +1,12 @@ --- -page_title: 'Attribute Custom Validators: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute custom validators description: >- - Validations check for required syntax, types, and acceptable values. Migrate custom attribute validation functions from SDKv2 to attribute validators in the Framework. + Learn how to migrate custom attribute validation functions from SDKv2 to + attribute validators in the Framework. Providers use custom validators to + check attribute values for required syntax, types, and acceptable values. --- -# Custom Validators +# Migrating attribute custom validators You can write custom validations that give users feedback about required syntax, types, and acceptable values in your provider. The Framework has a collection of 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 3f630ba03..33c32dfd4 100644 --- a/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx +++ b/website/docs/plugin/framework/migrating/attributes-blocks/validators-predefined.mdx @@ -1,11 +1,13 @@ --- -page_title: 'Attribute predefined validators: Migrating from SDKv2 to the Framework' +page_title: Migrating attribute predefined validators description: >- - Validations check required syntax, types, and acceptable values. - Migrate the predefined ConflictsWith, ExactlyOneOf, AtLeastOneOf and RequiredWith validators to the Framework. + Learn how to migrate the predefined ConflictsWith, ExactlyOneOf, AtLeastOneOf + and RequiredWith validators from SDKv2 to the framework. Providers use + predefined validators to check attribute values for required syntax, types, + and acceptable values. --- -# Validators - Predefined +# Migrating predefined attribute validators Attribute validators ensure that attributes do or do not contain specific values. You can use predefined validators for many use cases, or implement custom validators. Refer to [Schemas - Validators](/terraform/plugin/framework/handling-data/schemas#validators) in diff --git a/website/docs/plugin/framework/migrating/benefits.mdx b/website/docs/plugin/framework/migrating/benefits.mdx index 42b8e392d..847b2c73f 100644 --- a/website/docs/plugin/framework/migrating/benefits.mdx +++ b/website/docs/plugin/framework/migrating/benefits.mdx @@ -1,10 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Migration Benefits' +page_title: Benefits of migration description: >- - The plugin framework offers significant advantages in comparison to the prior SDK. + The plugin framework is an updated SDK for Terraform providers that includes + improved data access, more consistent schema models, and other improvements + over the previous SDKv2. --- -# Framework and SDKv2 Feature Comparison +# 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 91984bd70..eaa78e446 100644 --- a/website/docs/plugin/framework/migrating/data-sources/index.mdx +++ b/website/docs/plugin/framework/migrating/data-sources/index.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Data Sources: Migrating from SDKv2 to the Framework' +page_title: Migrating data sources description: >- - Migrate a data source from SDKv2 to the plugin Framework. + Learn how to migrate a data source from SDKv2 to the plugin framework. --- -# Data Sources +# Migrating data sources Data sources let Terraform reference external data. Unlike resources, Terraform does not create, update, or delete data sources, and makes no attempt to modify the underlying API. Data Sources are a read-only resource type, so they diff --git a/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx b/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx index 4d1200ebf..7b0105316 100644 --- a/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx +++ b/website/docs/plugin/framework/migrating/data-sources/timeouts.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Timeouts' +page_title: Migrating timeouts description: >- - How to migrate timeouts from SDKv2 to the Framework. + Learn how to migrate timeouts from SDKv2 to the framework. --- -# Timeouts +# 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 880f6c6bc..f13455039 100644 --- a/website/docs/plugin/framework/migrating/index.mdx +++ b/website/docs/plugin/framework/migrating/index.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development: Migrating from SDKv2 to the plugin Framework' +page_title: Migrating from SDKv2 to the plugin framework description: >- - Migrate your provider from SDKv2 to the plugin Framework. + Learn how to migrate your provider from SDKv2 to the plugin framework. --- # Overview @@ -16,7 +16,7 @@ In addition to this migration guide, we recommend referring to the main [Framewo Before you migrate your provider to the Framework, ensure it meets the following requirements: -- Go 1.21+ +- Go 1.22+ - Built on the latest version of SDKv2 - The provider is for use with Terraform >= 0.12.0 diff --git a/website/docs/plugin/framework/migrating/mux.mdx b/website/docs/plugin/framework/migrating/mux.mdx index 51cb55989..e0e57c4b7 100644 --- a/website/docs/plugin/framework/migrating/mux.mdx +++ b/website/docs/plugin/framework/migrating/mux.mdx @@ -1,7 +1,8 @@ --- -page_title: 'Plugin Development - Framework: Migration Using Mux' +page_title: Migration using muxing description: >- - Iteratively migrate from terraform-plugin-sdk to terraform-plugin-framework using terraform-plugin-mux. + Learn how to iteratively migrate from the SDKv2 to the plugin framework using + the terraform-plugin-mux Go library. --- # Muxing diff --git a/website/docs/plugin/framework/migrating/providers/index.mdx b/website/docs/plugin/framework/migrating/providers/index.mdx index 87142add9..6bc14ba9a 100644 --- a/website/docs/plugin/framework/migrating/providers/index.mdx +++ b/website/docs/plugin/framework/migrating/providers/index.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Provider: Migrating from SDKv2 to the Framework' +page_title: Migrating providers from SDKv2 to the framework description: >- - Migrate a provider definition and schema from SDKv2 to the plugin Framework. + Learn how to migrate a provider definition and schema from SDKv2 to the plugin + framework. --- -# Provider +# Migrating providers Providers are Terraform plugins that define resources and data sources for practitioners to use. You serve your providers with a provider server so they can interact with Terraform. diff --git a/website/docs/plugin/framework/migrating/resources/crud.mdx b/website/docs/plugin/framework/migrating/resources/crud.mdx index 80c875baa..645c0aae3 100644 --- a/website/docs/plugin/framework/migrating/resources/crud.mdx +++ b/website/docs/plugin/framework/migrating/resources/crud.mdx @@ -1,7 +1,8 @@ --- -page_title: 'Resources - CRUD Functions: Migrating from SDKv2 to the Framework' +page_title: CRUD functions description: >- - Migrate resource create, read, update, and delete (CRUD) functions from SDKv2 to the plugin Framework. + Learn how to migrate resource create, read, update, and delete (CRUD) + functions from SDKv2 to the plugin framework. --- # CRUD functions diff --git a/website/docs/plugin/framework/migrating/resources/import.mdx b/website/docs/plugin/framework/migrating/resources/import.mdx index f0af77b2a..c7ebb97aa 100644 --- a/website/docs/plugin/framework/migrating/resources/import.mdx +++ b/website/docs/plugin/framework/migrating/resources/import.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Resources - Import: Migrating from SDKv2 to the Framework' +page_title: Resource import description: >- - Practitioners use the import command to let Terraform manage existing infrastructure resources. - Migrate import functions from SDKv2 to the plugin Framework. + Learn how to migrate resource import functions from SDKv2 to the plugin + framework. Practitioners import resources to bring them under the control of + their Terraform projects. --- -# Import +# Resource import Practitioners can use the [`terraform import` command](/terraform/cli/commands/import) to let Terraform begin managing existing infrastructure by importing an existing resource into their Terraform project's state. A diff --git a/website/docs/plugin/framework/migrating/resources/index.mdx b/website/docs/plugin/framework/migrating/resources/index.mdx index 1340191c4..73d383a3a 100644 --- a/website/docs/plugin/framework/migrating/resources/index.mdx +++ b/website/docs/plugin/framework/migrating/resources/index.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Resources: Migrating from SDKv2 to the Framework' +page_title: Migrating resources description: >- - Migrate a resource from SDKv2 to the plugin Framework. + Learn how to migrate resources from SDKv2 to the plugin framework. --- -# Resources +# Migrating resources Resources are an abstraction that allow Terraform to manage infrastructure objects by defining create, read, update, and delete functionality that maps onto API operations. Resource schemas define what fields a resource has, give diff --git a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx index c8f6c9b7e..af8241279 100644 --- a/website/docs/plugin/framework/migrating/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/migrating/resources/plan-modification.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Resources - CustomizeDiff and PlanModifiers: Migrating from SDKv2 to the Framework' +page_title: Plan modification description: >- - Migrate resource customizediff functions in SDKv2 to plan modifiers in the plugin Framework. + Learn how to migrate resource CustomizeDiff functions in SDKv2 to + plan modifiers in the Terraform plugin framework. --- -# Plan Modification +# Plan modification Your provider can modify the Terraform plan to match the expected end state. This can include replacing unknown values with expected known values or marking a resource that must be replaced. Refer to diff --git a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx index e643b8e1e..7b5c05b33 100644 --- a/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx +++ b/website/docs/plugin/framework/migrating/resources/state-upgrade.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Resources - State Upgrading: Migrating from SDKv2 to the Framework' +page_title: State upgrading description: >- - State upgraders let users update resources provisioned with old schema configurations. - Migrate resource StateUpgraders in SDKv2 to UpgradeState in the plugin Framework. + Learn how to Migrate resource StateUpgraders in SDKv2 to UpgradeState in the + plugin framework. State upgraders let users update resources provisioned with + old schema configurations. --- -# State Upgraders +# State upgraders When you update a resource's implementation in your provider, some changes may not be compatible with old versions. You can create state upgraders to automatically migrate resources provisioned with old schema configurations. Refer to diff --git a/website/docs/plugin/framework/migrating/resources/timeouts.mdx b/website/docs/plugin/framework/migrating/resources/timeouts.mdx index 78483d34b..a7a8d76c2 100644 --- a/website/docs/plugin/framework/migrating/resources/timeouts.mdx +++ b/website/docs/plugin/framework/migrating/resources/timeouts.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Timeouts' +page_title: Timeouts description: >- - How to migrate timeouts from SDKv2 to the Framework. + Learn how to migrate timeouts from SDKv2 to the framework. --- # Timeouts diff --git a/website/docs/plugin/framework/migrating/schema/index.mdx b/website/docs/plugin/framework/migrating/schema/index.mdx index aa8ffdbab..5afd215f2 100644 --- a/website/docs/plugin/framework/migrating/schema/index.mdx +++ b/website/docs/plugin/framework/migrating/schema/index.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Schema: Migrating from SDKv2 to the Framework' +page_title: Migrating schema description: >- - Migrate a schema from SDKv2 to the plugin Framework. + Learn how to migrate schema from SDKv2 to the plugin framework. --- -# Schema +# Migrating schema Providers, resources, and data sources all use schema to define their attributes and behavior. Schemas specify the constraints of Terraform configuration blocks and how the provider, resource, or data source behaves. Refer to diff --git a/website/docs/plugin/framework/migrating/testing.mdx b/website/docs/plugin/framework/migrating/testing.mdx index 843772c6f..79eb6ea74 100644 --- a/website/docs/plugin/framework/migrating/testing.mdx +++ b/website/docs/plugin/framework/migrating/testing.mdx @@ -1,7 +1,8 @@ --- -page_title: 'Testing Migration: Migrating from SDKv2 to the Framework' +page_title: Testing migration description: >- - Write tests that verify that switching from SDKv2 to the Framework does not affect provider behavior. + Learn how to write tests that verify that migrating from SDKv2 to the + Framework does not affect provider behavior. --- # Testing diff --git a/website/docs/plugin/framework/provider-servers.mdx b/website/docs/plugin/framework/provider-servers.mdx index f00b650bf..a8d07bd5e 100644 --- a/website/docs/plugin/framework/provider-servers.mdx +++ b/website/docs/plugin/framework/provider-servers.mdx @@ -1,11 +1,12 @@ --- -page_title: 'Plugin Development - Framework: Provider Servers' +page_title: Provider servers description: >- - How to implement a provider server in the provider development framework. - Provider servers are plugins that allow Terraform to interact with APIs. + Learn how to implement a provider server in the Terraform plugin + framework. Provider servers are plugins that allow Terraform to interact with + APIs. --- -# Provider Servers +# 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 dc3352c33..131047f55 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -1,9 +1,9 @@ --- -page_title: 'Plugin Development - Framework: Providers' +page_title: Providers description: >- - How to implement a provider in the provider development framework. Providers, - wrapped by a provider server, are plugins that allow Terraform to interact - with APIs. + Learn how to implement a provider in the Terraform plugin framework. + Providers, wrapped by a provider server, are plugins that allow Terraform to + interact with APIs. --- # Providers @@ -14,6 +14,7 @@ This page describes the basic implementation details required for defining a pro - [Configure data sources](/terraform/plugin/framework/data-sources/configure) with provider-level data types or clients. - [Configure resources](/terraform/plugin/framework/resources/configure) with provider-level data types or clients. +- [Configure ephemeral resources](/terraform/plugin/framework/ephemeral-resources/configure) with provider-level data types or clients. - [Validate](/terraform/plugin/framework/providers/validate-configuration) practitioner configuration against acceptable values. ## Define Provider Type @@ -196,8 +197,8 @@ func (p *ExampleCloudProvider) Configure(ctx context.Context, req provider.Confi // Not returning early allows the logic to collect all errors. } - // Create data/clients and persist to resp.DataSourceData and - // resp.ResourceData as appropriate. + // Create data/clients and persist to resp.DataSourceData, resp.ResourceData, + // and resp.EphemeralResourceData as appropriate. } ``` diff --git a/website/docs/plugin/framework/providers/validate-configuration.mdx b/website/docs/plugin/framework/providers/validate-configuration.mdx index de1f2133b..f59330f1c 100644 --- a/website/docs/plugin/framework/providers/validate-configuration.mdx +++ b/website/docs/plugin/framework/providers/validate-configuration.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Validate Provider Configurations' +page_title: Validate provider configuration description: >- - How to validate provider configurations with the provider development framework. + Learn how to validate provider configurations with the Terraform plugin + framework. --- -# Validate Configuration +# 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 b828bea02..93dced950 100644 --- a/website/docs/plugin/framework/resources/configure.mdx +++ b/website/docs/plugin/framework/resources/configure.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Configure Resources' +page_title: Configure resources description: >- - How to configure resources with provider data or clients in the provider development framework. + Learn how to implement resource configuration with provider or client data in + the Terraform plugin framework. --- -# Configure Resources +# 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 0c41468e2..db9ec0914 100644 --- a/website/docs/plugin/framework/resources/create.mdx +++ b/website/docs/plugin/framework/resources/create.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Create Resources' +page_title: Create resources description: >- - How to implement resource creation in the provider development framework. + Learn how to implement resource creation in the Terraform plugin framework. --- -# Create Resources +# 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 2787a5e8b..5254d5c5b 100644 --- a/website/docs/plugin/framework/resources/default.mdx +++ b/website/docs/plugin/framework/resources/default.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Default' +page_title: Default values description: >- - How to set default values using the provider development framework. + Learn how to set default values for resource attributes with the Terraform + plugin framework. --- -# Default +# 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 d65986cdb..37c706e7f 100644 --- a/website/docs/plugin/framework/resources/delete.mdx +++ b/website/docs/plugin/framework/resources/delete.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Delete Resources' +page_title: Delete resources description: >- - How to implement resource deletion in the provider development framework. + Learn how to implement resource deletion in the Terraform plugin framework. --- -# Delete Resources +# 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 c0e719d2d..f8202686d 100644 --- a/website/docs/plugin/framework/resources/import.mdx +++ b/website/docs/plugin/framework/resources/import.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Resource Import' +page_title: Resource import description: >- - How to support resource import using the provider development framework. + Learn how to support resource import using the Terraform plugin framework. --- -# Resource Import +# 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 c7f0691b8..6aa9000c8 100644 --- a/website/docs/plugin/framework/resources/index.mdx +++ b/website/docs/plugin/framework/resources/index.mdx @@ -1,8 +1,8 @@ --- -page_title: 'Plugin Development - Framework: Resources' +page_title: Resources description: >- - How to build resources in the provider development framework. Resources allow - Terraform to manage infrastructure objects. + Learn how to build resources in the Terraform plugin framework. Resources + allow Terraform to manage infrastructure objects. --- # Resources @@ -30,6 +30,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 7da986333..bcab40d8d 100644 --- a/website/docs/plugin/framework/resources/plan-modification.mdx +++ b/website/docs/plugin/framework/resources/plan-modification.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Plan Modification' +page_title: Plan modification description: >- - How to modify plan values and behaviors using the provider development + Learn how to modify plan values and behaviors with the Terraform plugin framework. --- -# Plan Modification +# 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 43c84cf47..244b1a9b9 100644 --- a/website/docs/plugin/framework/resources/private-state.mdx +++ b/website/docs/plugin/framework/resources/private-state.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Private State Management' +page_title: Private state management description: >- - How to manage private state data in the provider development framework. + Learn how to manage private state data in the Terraform plugin framework. Private state is provider-only data storage for resources. --- -# Private State Management +# 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 f9e6bed80..dc0161ae9 100644 --- a/website/docs/plugin/framework/resources/read.mdx +++ b/website/docs/plugin/framework/resources/read.mdx @@ -1,10 +1,10 @@ --- -page_title: 'Plugin Development - Framework: Read Resources' +page_title: Read resources description: >- - How to implement resource read in the provider development framework. + Learn how to implement resource read in the Terraform plugin framework. --- -# Read Resources +# 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 8d1e4eb4c..c97dc57cd 100644 --- a/website/docs/plugin/framework/resources/state-move.mdx +++ b/website/docs/plugin/framework/resources/state-move.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: State Move' +page_title: State move description: >- - How to move state data across managed resource types using the provider - development framework. + Learn how to implement moving state data across managed resource types using + the Terraform plugin framework. --- -# State Move +# State move diff --git a/website/docs/plugin/framework/resources/state-upgrade.mdx b/website/docs/plugin/framework/resources/state-upgrade.mdx index a1cdc1db6..31fd5c678 100644 --- a/website/docs/plugin/framework/resources/state-upgrade.mdx +++ b/website/docs/plugin/framework/resources/state-upgrade.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Plugin Development - Framework: State Upgrade' +page_title: State upgrade description: >- - How to upgrade state data after breaking schema changes using the provider - development framework. + Learn how to implement upgrading state data when provider schema changes from + one version of your Terraform framework provider to another. --- -# State Upgrade +# 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 a84cc117e..db14bae0c 100644 --- a/website/docs/plugin/framework/resources/timeouts.mdx +++ b/website/docs/plugin/framework/resources/timeouts.mdx @@ -1,7 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Timeouts' +page_title: Timeouts description: >- - How to use timeouts with the provider development framework. + Learn how to implement timeouts with the Terraform plugin framework. --- # Timeouts diff --git a/website/docs/plugin/framework/resources/update.mdx b/website/docs/plugin/framework/resources/update.mdx index 8701a0b5e..a892b4161 100644 --- a/website/docs/plugin/framework/resources/update.mdx +++ b/website/docs/plugin/framework/resources/update.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Update Resources' +page_title: Update resources description: >- - How to implement resource in-place update in the provider development framework. + Learn how to implement in-place updating of resources in the Terraform + plugin framework. --- -# Update Resources +# 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 fa2b22eaf..4a1d93661 100644 --- a/website/docs/plugin/framework/resources/validate-configuration.mdx +++ b/website/docs/plugin/framework/resources/validate-configuration.mdx @@ -1,10 +1,11 @@ --- -page_title: 'Plugin Development - Framework: Validate Resource Configurations' +page_title: Validate resource configuration description: >- - How to validate resource configurations with the provider development framework. + Learn how to validate resource configuration with the Terraform plugin + framework. --- -# Validate Configuration +# 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..ee61e2b99 --- /dev/null +++ b/website/docs/plugin/framework/resources/write-only-arguments.mdx @@ -0,0 +1,114 @@ +--- +page_title: 'Plugin Development - Framework: Write-only Arguments' +description: >- + How to implement write-only arguments with the provider development framework. +--- + +# 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 + +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 b0349fd8a..fedfbafb9 100644 --- a/website/docs/plugin/framework/validation.mdx +++ b/website/docs/plugin/framework/validation.mdx @@ -1,6 +1,7 @@ --- -page_title: 'Plugin Development - Framework: Validation' -description: How to validate configuration values using the provider development framework. +page_title: Validation +description: >- + Learn how to validate configuration values using the Terraform plugin framework. --- # Validation @@ -12,10 +13,11 @@ This page describes single attribute, parameter, and type validation concepts th - [Data source validation](/terraform/plugin/framework/data-sources/validate-configuration) for multiple attributes declaratively or imperatively. - [Provider validation](/terraform/plugin/framework/providers/validate-configuration) for multiple attributes declaratively or imperatively. - [Resource validation](/terraform/plugin/framework/resources/validate-configuration) for multiple attributes declaratively or imperatively. +- [Ephemeral Resource validation](/terraform/plugin/framework/ephemeral-resources/validate-configuration) for multiple attributes declaratively or imperatively. -> **Note:** When implementing validation logic, configuration values may be [unknown](/terraform/plugin/framework/types#unknown) based on the source of the value. Implementations must account for this case, typically by returning early without returning new diagnostics. -During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan), [`terraform apply`](/terraform/cli/commands/apply) and [`terraform destroy`](/terraform/cli/commands/destroy) commands, Terraform calls the provider [`ValidateProviderConfig`](/terraform/plugin/framework/internals/rpcs#validateproviderconfig-rpc), [`ValidateResourceConfig`](/terraform/plugin/framework/internals/rpcs#validateresourceconfig-rpc) and [`ValidateDataResourceConfig`](/terraform/plugin/framework/internals/rpcs#validatedataresourceconfig-rpc) RPCs. +During execution of the [`terraform validate`](/terraform/cli/commands/validate), [`terraform plan`](/terraform/cli/commands/plan), [`terraform apply`](/terraform/cli/commands/apply) and [`terraform destroy`](/terraform/cli/commands/destroy) commands, Terraform calls the provider [`ValidateProviderConfig`](/terraform/plugin/framework/internals/rpcs#validateproviderconfig-rpc), [`ValidateResourceConfig`](/terraform/plugin/framework/internals/rpcs#validateresourceconfig-rpc), [`ValidateDataResourceConfig`](/terraform/plugin/framework/internals/rpcs#validatedataresourceconfig-rpc), and `ValidateEphemeralResourceConfig` RPCs. ## Default Terraform CLI Validation