From ffca03d9f3729816aaaaf4a78c92469c0afe12df Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:50:03 -0400 Subject: [PATCH 1/6] Result of tsccr-helper -log-level=info gha update -latest . (#1045) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-changie.yml | 2 +- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 12 ++++++------ .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/compliance.yml | 2 +- .github/workflows/release.yml | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml index de5554dda..60a514c03 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 # 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..2358e4adb 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,7 +13,7 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 3515456ec..05167f4f0 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,7 +16,7 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' @@ -29,8 +29,8 @@ 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner @@ -55,8 +55,8 @@ 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner @@ -81,7 +81,7 @@ jobs: matrix: go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 0603e6b35..d978baa4c 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,7 +14,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index f745aa05d..d09653e1c 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - 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..e253c38df 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 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,7 +79,7 @@ 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 From bca1b06f666d7d41ab3b62684b23bec424404317 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:52:51 -0400 Subject: [PATCH 2/6] Result of tsccr-helper -log-level=info gha update -latest . (#1046) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 05167f4f0..a078f9aad 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,7 +21,7 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1 terraform-provider-corner-tfprotov5: defaults: run: From b372415f8e8aec4de386eebba11d954d2e2c6fda Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:59:45 -0400 Subject: [PATCH 3/6] Result of tsccr-helper -log-level=info gha update -latest . (#1048) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-changie.yml | 2 +- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 14 +++++++------- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/compliance.yml | 2 +- .github/workflows/release.yml | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml index 60a514c03..33ba8ee87 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 # 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 2358e4adb..47f1bbb64 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,7 +13,7 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index a078f9aad..156e401d6 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,7 +16,7 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' @@ -29,8 +29,8 @@ jobs: name: terraform-provider-corner (tfprotov5 / Terraform ${{ matrix.terraform}}) runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner @@ -55,8 +55,8 @@ jobs: name: terraform-provider-corner (tfprotov6 / Terraform ${{ matrix.terraform}}) runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner @@ -81,14 +81,14 @@ jobs: matrix: go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 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 d978baa4c..9e5193bc9 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,7 +14,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 with: go-version-file: 'go.mod' diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index d09653e1c..4de30ea9d 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - 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 e253c38df..4b3613dcf 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 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@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 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,7 +79,7 @@ jobs: contents: write # Needed for goreleaser to create GitHub release issues: write # Needed for goreleaser to close associated milestone steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 From a2137d3a9897127bb07fddf771975e50f1946274 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:08:49 -0400 Subject: [PATCH 4/6] build(deps): Bump github.com/hashicorp/terraform-plugin-go (#1051) Bumps [github.com/hashicorp/terraform-plugin-go](https://github.com/hashicorp/terraform-plugin-go) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-go/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-go/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 18 +++++++++--------- go.sum | 39 +++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 8571a3290..193d48e39 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.24.0 + github.com/hashicorp/terraform-plugin-go v0.25.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.1 // 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-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.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/grpc v1.66.2 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect ) diff --git a/go.sum b/go.sum index 3fe9c7360..99e7874be 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ 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/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.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= -github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +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.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= -github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= +github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= +github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= github.com/hashicorp/terraform-plugin-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= @@ -31,8 +31,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 +41,31 @@ 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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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= From ae74f9389c44029fce3f09fab4c5ea466b210a6f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 31 Oct 2024 14:10:45 -0400 Subject: [PATCH 5/6] ephemeral: Initial ephemeral resource type implementation (#1050) * initial ephemeral resource interfaces * add ephemeral resource configure data * attribute implementations * uncomment custom type tests * added block implementations * add nested attribute implementations * add schema test * remove todo * doc updates, renames, removals * initial protov5 + fwserver implementation (protov6 stubbed) * add fromproto5 tests * add toproto5 tests * add proto5server tests * implement protov6 * schema + metadata tests * add close proto5/6 tests * add fwserver tests for schema/metadata * prevent random false positives * validate fwserver tests * open/renew/close fwserver tests * update error message * update plugin go * remove `config` from renew * remove state from renew/close, add deferred action support * update doc comments * add experimental note * add changelogs * initial website documentation * build(deps): Bump github.com/hashicorp/terraform-plugin-go (#1051) Bumps [github.com/hashicorp/terraform-plugin-go](https://github.com/hashicorp/terraform-plugin-go) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-go/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-go/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update website/docs/plugin/framework/ephemeral-resources/renew.mdx Co-authored-by: Selena Goods * Update website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx Co-authored-by: Selena Goods * adjust package docs --------- --- .../ENHANCEMENTS-20241028-130457.yaml | 6 + .../ENHANCEMENTS-20241028-130618.yaml | 6 + .../ENHANCEMENTS-20241028-130758.yaml | 6 + .../unreleased/FEATURES-20241028-130339.yaml | 5 + .../unreleased/FEATURES-20241028-130855.yaml | 5 + .../unreleased/NOTES-20241028-130308.yaml | 6 + ephemeral/close.go | 30 + ephemeral/config_validator.go | 28 + ephemeral/configure.go | 33 + ephemeral/deferred.go | 50 + ephemeral/doc.go | 26 + ephemeral/ephemeral_resource.go | 108 + ephemeral/metadata.go | 23 + ephemeral/open.go | 80 + ephemeral/renew.go | 51 + ephemeral/schema.go | 26 + ephemeral/schema/attribute.go | 39 + ephemeral/schema/block.go | 30 + ephemeral/schema/bool_attribute.go | 185 ++ ephemeral/schema/bool_attribute_test.go | 425 ++++ ephemeral/schema/doc.go | 11 + ephemeral/schema/dynamic_attribute.go | 186 ++ ephemeral/schema/dynamic_attribute_test.go | 425 ++++ ephemeral/schema/float32_attribute.go | 189 ++ ephemeral/schema/float32_attribute_test.go | 426 ++++ ephemeral/schema/float64_attribute.go | 188 ++ ephemeral/schema/float64_attribute_test.go | 425 ++++ ephemeral/schema/int32_attribute.go | 189 ++ ephemeral/schema/int32_attribute_test.go | 426 ++++ ephemeral/schema/int64_attribute.go | 188 ++ ephemeral/schema/int64_attribute_test.go | 425 ++++ ephemeral/schema/list_attribute.go | 220 ++ ephemeral/schema/list_attribute_test.go | 523 +++++ ephemeral/schema/list_nested_attribute.go | 244 +++ .../schema/list_nested_attribute_test.go | 690 ++++++ ephemeral/schema/list_nested_block.go | 205 ++ ephemeral/schema/list_nested_block_test.go | 570 +++++ ephemeral/schema/map_attribute.go | 223 ++ ephemeral/schema/map_attribute_test.go | 523 +++++ ephemeral/schema/map_nested_attribute.go | 245 +++ ephemeral/schema/map_nested_attribute_test.go | 690 ++++++ ephemeral/schema/nested_attribute.go | 14 + ephemeral/schema/nested_attribute_object.go | 82 + .../schema/nested_attribute_object_test.go | 280 +++ ephemeral/schema/nested_block_object.go | 94 + ephemeral/schema/nested_block_object_test.go | 366 ++++ ephemeral/schema/number_attribute.go | 189 ++ ephemeral/schema/number_attribute_test.go | 425 ++++ ephemeral/schema/object_attribute.go | 222 ++ ephemeral/schema/object_attribute_test.go | 556 +++++ ephemeral/schema/schema.go | 187 ++ ephemeral/schema/schema_test.go | 1357 ++++++++++++ ephemeral/schema/set_attribute.go | 218 ++ ephemeral/schema/set_attribute_test.go | 523 +++++ ephemeral/schema/set_nested_attribute.go | 240 +++ ephemeral/schema/set_nested_attribute_test.go | 690 ++++++ ephemeral/schema/set_nested_block.go | 205 ++ ephemeral/schema/set_nested_block_test.go | 570 +++++ ephemeral/schema/single_nested_attribute.go | 244 +++ .../schema/single_nested_attribute_test.go | 569 +++++ ephemeral/schema/single_nested_block.go | 213 ++ ephemeral/schema/single_nested_block_test.go | 485 +++++ ephemeral/schema/string_attribute.go | 185 ++ ephemeral/schema/string_attribute_test.go | 425 ++++ ephemeral/validate_config.go | 32 + internal/fromproto5/client_capabilities.go | 14 + internal/fromproto5/closeephemeralresource.go | 52 + .../fromproto5/closeephemeralresource_test.go | 101 + internal/fromproto5/ephemeral_result_data.go | 53 + .../fromproto5/ephemeral_result_data_test.go | 122 ++ internal/fromproto5/openephemeralresource.go | 52 + .../fromproto5/openephemeralresource_test.go | 146 ++ internal/fromproto5/renewephemeralresource.go | 52 + .../fromproto5/renewephemeralresource_test.go | 101 + .../validateephemeralresourceconfig.go | 31 + .../validateephemeralresourceconfig_test.go | 110 + internal/fromproto6/client_capabilities.go | 14 + internal/fromproto6/closeephemeralresource.go | 52 + .../fromproto6/closeephemeralresource_test.go | 101 + internal/fromproto6/ephemeral_result_data.go | 53 + .../fromproto6/ephemeral_result_data_test.go | 122 ++ internal/fromproto6/openephemeralresource.go | 52 + .../fromproto6/openephemeralresource_test.go | 146 ++ internal/fromproto6/renewephemeralresource.go | 52 + .../fromproto6/renewephemeralresource_test.go | 101 + .../validateephemeralresourceconfig.go | 31 + .../validateephemeralresourceconfig_test.go | 110 + internal/fwschemadata/data_description.go | 6 + internal/fwserver/server.go | 29 + .../fwserver/server_closeephemeralresource.go | 76 + .../server_closeephemeralresource_test.go | 206 ++ internal/fwserver/server_configureprovider.go | 1 + .../fwserver/server_configureprovider_test.go | 18 + .../fwserver/server_ephemeralresources.go | 198 ++ internal/fwserver/server_getmetadata.go | 14 + internal/fwserver/server_getmetadata_test.go | 208 +- internal/fwserver/server_getproviderschema.go | 25 +- .../fwserver/server_getproviderschema_test.go | 331 ++- .../fwserver/server_openephemeralresource.go | 113 + .../server_openephemeralresource_test.go | 402 ++++ .../fwserver/server_renewephemeralresource.go | 98 + .../server_renewephemeralresource_test.go | 283 +++ .../server_validateephemeralresourceconfig.go | 109 + ...er_validateephemeralresourceconfig_test.go | 308 +++ internal/logging/keys.go | 3 + .../server_closeephemeralresource.go | 50 + .../server_closeephemeralresource_test.go | 131 ++ .../proto5server/server_getmetadata_test.go | 167 +- .../server_getproviderschema_test.go | 263 ++- .../server_openephemeralresource.go | 50 + .../server_openephemeralresource_test.go | 268 +++ .../server_renewephemeralresource.go | 50 + .../server_renewephemeralresource_test.go | 165 ++ .../server_validateephemeralresourceconfig.go | 50 + ...er_validateephemeralresourceconfig_test.go | 168 ++ .../server_closeephemeralresource.go | 50 + .../server_closeephemeralresource_test.go | 131 ++ .../proto6server/server_getmetadata_test.go | 167 +- .../server_getproviderschema_test.go | 261 ++- .../server_openephemeralresource.go | 50 + .../server_openephemeralresource_test.go | 268 +++ .../server_renewephemeralresource.go | 50 + .../server_renewephemeralresource_test.go | 165 ++ .../server_validateephemeralresourceconfig.go | 50 + ...er_validateephemeralresourceconfig_test.go | 168 ++ .../testing/testprovider/ephemeralresource.go | 47 + .../ephemeralresourceconfigvalidator.go | 47 + .../ephemeralresourcewithclose.go | 30 + .../ephemeralresourcewithconfigure.go | 30 + .../ephemeralresourcewithconfigureandclose.go | 43 + .../ephemeralresourcewithconfigureandrenew.go | 43 + .../ephemeralresourcewithconfigvalidators.go | 30 + .../ephemeralresourcewithrenew.go | 30 + .../ephemeralresourcewithvalidateconfig.go | 30 + internal/testing/testprovider/provider.go | 20 +- internal/toproto5/closeephemeralresource.go | 25 + .../toproto5/closeephemeralresource_test.go | 69 + internal/toproto5/deferred.go | 10 + internal/toproto5/ephemeral_result_data.go | 28 + .../toproto5/ephemeral_result_data_test.go | 109 + .../toproto5/ephemeralresourcemetadata.go | 19 + .../ephemeralresourcemetadata_test.go | 46 + internal/toproto5/getmetadata.go | 15 +- internal/toproto5/getmetadata_test.go | 46 +- internal/toproto5/getproviderschema.go | 25 +- internal/toproto5/getproviderschema_test.go | 1821 +++++++++++++--- internal/toproto5/openephemeralresource.go | 37 + .../toproto5/openephemeralresource_test.go | 214 ++ internal/toproto5/renewephemeralresource.go | 31 + .../toproto5/renewephemeralresource_test.go | 117 + .../validateephemeralresourceconfig.go | 25 + .../validateephemeralresourceconfig_test.go | 69 + internal/toproto6/closeephemeralresource.go | 25 + .../toproto6/closeephemeralresource_test.go | 69 + internal/toproto6/deferred.go | 10 + internal/toproto6/ephemeral_result_data.go | 28 + .../toproto6/ephemeral_result_data_test.go | 109 + .../toproto6/ephemeralresourcemetadata.go | 19 + .../ephemeralresourcemetadata_test.go | 46 + internal/toproto6/getmetadata.go | 5 + internal/toproto6/getmetadata_test.go | 46 +- internal/toproto6/getproviderschema.go | 25 +- internal/toproto6/getproviderschema_test.go | 1878 ++++++++++++++--- internal/toproto6/openephemeralresource.go | 37 + .../toproto6/openephemeralresource_test.go | 214 ++ internal/toproto6/renewephemeralresource.go | 31 + .../toproto6/renewephemeralresource_test.go | 117 + .../validateephemeralresourceconfig.go | 25 + .../validateephemeralresourceconfig_test.go | 69 + provider/configure.go | 5 + provider/provider.go | 19 + tfsdk/ephemeral_result_data.go | 94 + tfsdk/ephemeral_result_data_test.go | 487 +++++ website/data/plugin-framework-nav-data.json | 29 + .../framework/ephemeral-resources/close.mdx | 94 + .../ephemeral-resources/configure.mdx | 103 + .../framework/ephemeral-resources/index.mdx | 101 + .../framework/ephemeral-resources/open.mdx | 76 + .../framework/ephemeral-resources/renew.mdx | 113 + .../validate-configuration.mdx | 85 + .../handling-data/attributes/bool.mdx | 1 + .../handling-data/attributes/dynamic.mdx | 1 + .../handling-data/attributes/float32.mdx | 1 + .../handling-data/attributes/float64.mdx | 1 + .../handling-data/attributes/int32.mdx | 1 + .../handling-data/attributes/int64.mdx | 1 + .../handling-data/attributes/list-nested.mdx | 1 + .../handling-data/attributes/list.mdx | 1 + .../handling-data/attributes/map-nested.mdx | 1 + .../handling-data/attributes/map.mdx | 1 + .../handling-data/attributes/number.mdx | 1 + .../handling-data/attributes/object.mdx | 1 + .../handling-data/attributes/set-nested.mdx | 1 + .../handling-data/attributes/set.mdx | 1 + .../attributes/single-nested.mdx | 1 + .../handling-data/attributes/string.mdx | 1 + .../handling-data/blocks/list-nested.mdx | 1 + .../handling-data/blocks/set-nested.mdx | 1 + .../handling-data/blocks/single-nested.mdx | 1 + .../framework/handling-data/schemas.mdx | 5 +- .../handling-data/terraform-concepts.mdx | 4 +- .../framework/handling-data/types/bool.mdx | 1 + .../framework/handling-data/types/dynamic.mdx | 1 + .../framework/handling-data/types/float32.mdx | 1 + .../framework/handling-data/types/float64.mdx | 1 + .../framework/handling-data/types/int32.mdx | 1 + .../framework/handling-data/types/int64.mdx | 1 + .../framework/handling-data/types/list.mdx | 3 + .../framework/handling-data/types/map.mdx | 4 +- .../framework/handling-data/types/number.mdx | 1 + .../framework/handling-data/types/object.mdx | 3 + .../framework/handling-data/types/set.mdx | 3 + .../framework/handling-data/types/string.mdx | 1 + .../framework/handling-data/types/tuple.mdx | 2 +- .../docs/plugin/framework/providers/index.mdx | 5 +- website/docs/plugin/framework/validation.mdx | 3 +- 216 files changed, 29854 insertions(+), 788 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130457.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130618.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130758.yaml create mode 100644 .changes/unreleased/FEATURES-20241028-130339.yaml create mode 100644 .changes/unreleased/FEATURES-20241028-130855.yaml create mode 100644 .changes/unreleased/NOTES-20241028-130308.yaml create mode 100644 ephemeral/close.go create mode 100644 ephemeral/config_validator.go create mode 100644 ephemeral/configure.go create mode 100644 ephemeral/deferred.go create mode 100644 ephemeral/doc.go create mode 100644 ephemeral/ephemeral_resource.go create mode 100644 ephemeral/metadata.go create mode 100644 ephemeral/open.go create mode 100644 ephemeral/renew.go create mode 100644 ephemeral/schema.go create mode 100644 ephemeral/schema/attribute.go create mode 100644 ephemeral/schema/block.go create mode 100644 ephemeral/schema/bool_attribute.go create mode 100644 ephemeral/schema/bool_attribute_test.go create mode 100644 ephemeral/schema/doc.go create mode 100644 ephemeral/schema/dynamic_attribute.go create mode 100644 ephemeral/schema/dynamic_attribute_test.go create mode 100644 ephemeral/schema/float32_attribute.go create mode 100644 ephemeral/schema/float32_attribute_test.go create mode 100644 ephemeral/schema/float64_attribute.go create mode 100644 ephemeral/schema/float64_attribute_test.go create mode 100644 ephemeral/schema/int32_attribute.go create mode 100644 ephemeral/schema/int32_attribute_test.go create mode 100644 ephemeral/schema/int64_attribute.go create mode 100644 ephemeral/schema/int64_attribute_test.go create mode 100644 ephemeral/schema/list_attribute.go create mode 100644 ephemeral/schema/list_attribute_test.go create mode 100644 ephemeral/schema/list_nested_attribute.go create mode 100644 ephemeral/schema/list_nested_attribute_test.go create mode 100644 ephemeral/schema/list_nested_block.go create mode 100644 ephemeral/schema/list_nested_block_test.go create mode 100644 ephemeral/schema/map_attribute.go create mode 100644 ephemeral/schema/map_attribute_test.go create mode 100644 ephemeral/schema/map_nested_attribute.go create mode 100644 ephemeral/schema/map_nested_attribute_test.go create mode 100644 ephemeral/schema/nested_attribute.go create mode 100644 ephemeral/schema/nested_attribute_object.go create mode 100644 ephemeral/schema/nested_attribute_object_test.go create mode 100644 ephemeral/schema/nested_block_object.go create mode 100644 ephemeral/schema/nested_block_object_test.go create mode 100644 ephemeral/schema/number_attribute.go create mode 100644 ephemeral/schema/number_attribute_test.go create mode 100644 ephemeral/schema/object_attribute.go create mode 100644 ephemeral/schema/object_attribute_test.go create mode 100644 ephemeral/schema/schema.go create mode 100644 ephemeral/schema/schema_test.go create mode 100644 ephemeral/schema/set_attribute.go create mode 100644 ephemeral/schema/set_attribute_test.go create mode 100644 ephemeral/schema/set_nested_attribute.go create mode 100644 ephemeral/schema/set_nested_attribute_test.go create mode 100644 ephemeral/schema/set_nested_block.go create mode 100644 ephemeral/schema/set_nested_block_test.go create mode 100644 ephemeral/schema/single_nested_attribute.go create mode 100644 ephemeral/schema/single_nested_attribute_test.go create mode 100644 ephemeral/schema/single_nested_block.go create mode 100644 ephemeral/schema/single_nested_block_test.go create mode 100644 ephemeral/schema/string_attribute.go create mode 100644 ephemeral/schema/string_attribute_test.go create mode 100644 ephemeral/validate_config.go create mode 100644 internal/fromproto5/closeephemeralresource.go create mode 100644 internal/fromproto5/closeephemeralresource_test.go create mode 100644 internal/fromproto5/ephemeral_result_data.go create mode 100644 internal/fromproto5/ephemeral_result_data_test.go create mode 100644 internal/fromproto5/openephemeralresource.go create mode 100644 internal/fromproto5/openephemeralresource_test.go create mode 100644 internal/fromproto5/renewephemeralresource.go create mode 100644 internal/fromproto5/renewephemeralresource_test.go create mode 100644 internal/fromproto5/validateephemeralresourceconfig.go create mode 100644 internal/fromproto5/validateephemeralresourceconfig_test.go create mode 100644 internal/fromproto6/closeephemeralresource.go create mode 100644 internal/fromproto6/closeephemeralresource_test.go create mode 100644 internal/fromproto6/ephemeral_result_data.go create mode 100644 internal/fromproto6/ephemeral_result_data_test.go create mode 100644 internal/fromproto6/openephemeralresource.go create mode 100644 internal/fromproto6/openephemeralresource_test.go create mode 100644 internal/fromproto6/renewephemeralresource.go create mode 100644 internal/fromproto6/renewephemeralresource_test.go create mode 100644 internal/fromproto6/validateephemeralresourceconfig.go create mode 100644 internal/fromproto6/validateephemeralresourceconfig_test.go create mode 100644 internal/fwserver/server_closeephemeralresource.go create mode 100644 internal/fwserver/server_closeephemeralresource_test.go create mode 100644 internal/fwserver/server_ephemeralresources.go create mode 100644 internal/fwserver/server_openephemeralresource.go create mode 100644 internal/fwserver/server_openephemeralresource_test.go create mode 100644 internal/fwserver/server_renewephemeralresource.go create mode 100644 internal/fwserver/server_renewephemeralresource_test.go create mode 100644 internal/fwserver/server_validateephemeralresourceconfig.go create mode 100644 internal/fwserver/server_validateephemeralresourceconfig_test.go create mode 100644 internal/proto5server/server_closeephemeralresource.go create mode 100644 internal/proto5server/server_closeephemeralresource_test.go create mode 100644 internal/proto5server/server_openephemeralresource.go create mode 100644 internal/proto5server/server_openephemeralresource_test.go create mode 100644 internal/proto5server/server_renewephemeralresource.go create mode 100644 internal/proto5server/server_renewephemeralresource_test.go create mode 100644 internal/proto5server/server_validateephemeralresourceconfig.go create mode 100644 internal/proto5server/server_validateephemeralresourceconfig_test.go create mode 100644 internal/proto6server/server_closeephemeralresource.go create mode 100644 internal/proto6server/server_closeephemeralresource_test.go create mode 100644 internal/proto6server/server_openephemeralresource.go create mode 100644 internal/proto6server/server_openephemeralresource_test.go create mode 100644 internal/proto6server/server_renewephemeralresource.go create mode 100644 internal/proto6server/server_renewephemeralresource_test.go create mode 100644 internal/proto6server/server_validateephemeralresourceconfig.go create mode 100644 internal/proto6server/server_validateephemeralresourceconfig_test.go create mode 100644 internal/testing/testprovider/ephemeralresource.go create mode 100644 internal/testing/testprovider/ephemeralresourceconfigvalidator.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithclose.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigure.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigureandclose.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigureandrenew.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithconfigvalidators.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithrenew.go create mode 100644 internal/testing/testprovider/ephemeralresourcewithvalidateconfig.go create mode 100644 internal/toproto5/closeephemeralresource.go create mode 100644 internal/toproto5/closeephemeralresource_test.go create mode 100644 internal/toproto5/ephemeral_result_data.go create mode 100644 internal/toproto5/ephemeral_result_data_test.go create mode 100644 internal/toproto5/ephemeralresourcemetadata.go create mode 100644 internal/toproto5/ephemeralresourcemetadata_test.go create mode 100644 internal/toproto5/openephemeralresource.go create mode 100644 internal/toproto5/openephemeralresource_test.go create mode 100644 internal/toproto5/renewephemeralresource.go create mode 100644 internal/toproto5/renewephemeralresource_test.go create mode 100644 internal/toproto5/validateephemeralresourceconfig.go create mode 100644 internal/toproto5/validateephemeralresourceconfig_test.go create mode 100644 internal/toproto6/closeephemeralresource.go create mode 100644 internal/toproto6/closeephemeralresource_test.go create mode 100644 internal/toproto6/ephemeral_result_data.go create mode 100644 internal/toproto6/ephemeral_result_data_test.go create mode 100644 internal/toproto6/ephemeralresourcemetadata.go create mode 100644 internal/toproto6/ephemeralresourcemetadata_test.go create mode 100644 internal/toproto6/openephemeralresource.go create mode 100644 internal/toproto6/openephemeralresource_test.go create mode 100644 internal/toproto6/renewephemeralresource.go create mode 100644 internal/toproto6/renewephemeralresource_test.go create mode 100644 internal/toproto6/validateephemeralresourceconfig.go create mode 100644 internal/toproto6/validateephemeralresourceconfig_test.go create mode 100644 tfsdk/ephemeral_result_data.go create mode 100644 tfsdk/ephemeral_result_data_test.go create mode 100644 website/docs/plugin/framework/ephemeral-resources/close.mdx create mode 100644 website/docs/plugin/framework/ephemeral-resources/configure.mdx create mode 100644 website/docs/plugin/framework/ephemeral-resources/index.mdx create mode 100644 website/docs/plugin/framework/ephemeral-resources/open.mdx create mode 100644 website/docs/plugin/framework/ephemeral-resources/renew.mdx create mode 100644 website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx diff --git a/.changes/unreleased/ENHANCEMENTS-20241028-130457.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130457.yaml new file mode 100644 index 000000000..d477f1130 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20241028-130457.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'provider: Added `ProviderWithEphemeralResources` interface for implementing + ephemeral resources' +time: 2024-10-28T13:04:57.796703-04:00 +custom: + Issue: "1050" diff --git a/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml new file mode 100644 index 000000000..f52d2d14e --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'tfsdk: Added `EphemeralResultData` struct for representing ephemeral values + produced by a provider, such as from an ephemeral resource' +time: 2024-10-28T13:06:18.799164-04:00 +custom: + Issue: "1050" diff --git a/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml new file mode 100644 index 000000000..720293494 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'provider: Added `EphemeralResourceData` to `ConfigureResponse`, to pass provider-defined + data to `ephemeral.EphemeralResource` implementations' +time: 2024-10-28T13:07:58.9914-04:00 +custom: + Issue: "1050" diff --git a/.changes/unreleased/FEATURES-20241028-130339.yaml b/.changes/unreleased/FEATURES-20241028-130339.yaml new file mode 100644 index 000000000..04c58e75d --- /dev/null +++ b/.changes/unreleased/FEATURES-20241028-130339.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'ephemeral: New package for implementing ephemeral resources' +time: 2024-10-28T13:03:39.23218-04:00 +custom: + Issue: "1050" diff --git a/.changes/unreleased/FEATURES-20241028-130855.yaml b/.changes/unreleased/FEATURES-20241028-130855.yaml new file mode 100644 index 000000000..57d93a69d --- /dev/null +++ b/.changes/unreleased/FEATURES-20241028-130855.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'ephemeral/schema: New package for implementing ephemeral resource schemas' +time: 2024-10-28T13:08:55.520004-04:00 +custom: + Issue: "1050" diff --git a/.changes/unreleased/NOTES-20241028-130308.yaml b/.changes/unreleased/NOTES-20241028-130308.yaml new file mode 100644 index 000000000..5c9ca5240 --- /dev/null +++ b/.changes/unreleased/NOTES-20241028-130308.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: Ephemeral resource support is in technical preview and offered without compatibility + promises until Terraform 1.10 is generally available. +time: 2024-10-28T13:03:08.373897-04:00 +custom: + Issue: "1050" diff --git a/ephemeral/close.go b/ephemeral/close.go new file mode 100644 index 000000000..3d1745853 --- /dev/null +++ b/ephemeral/close.go @@ -0,0 +1,30 @@ +// 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..782718d8d --- /dev/null +++ b/ephemeral/config_validator.go @@ -0,0 +1,28 @@ +// 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..bcf6dd908 --- /dev/null +++ b/ephemeral/configure.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" +) + +// 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..2537d610c --- /dev/null +++ b/ephemeral/doc.go @@ -0,0 +1,26 @@ +// 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. +// +// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until +// these notices are removed. +package ephemeral diff --git a/ephemeral/ephemeral_resource.go b/ephemeral/ephemeral_resource.go new file mode 100644 index 000000000..1261de52d --- /dev/null +++ b/ephemeral/ephemeral_resource.go @@ -0,0 +1,108 @@ +// 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. +// +// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until +// these notices are removed. +type EphemeralResource interface { + // Metadata should return the full name of the ephemeral resource, such as + // examplecloud_thing. + 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..ed97522b8 --- /dev/null +++ b/ephemeral/metadata.go @@ -0,0 +1,23 @@ +// 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..5da47ee1c --- /dev/null +++ b/ephemeral/open.go @@ -0,0 +1,80 @@ +// 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..dd7f0c8e3 --- /dev/null +++ b/ephemeral/renew.go @@ -0,0 +1,51 @@ +// 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..c49067975 --- /dev/null +++ b/ephemeral/schema.go @@ -0,0 +1,26 @@ +// 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..47e248aae --- /dev/null +++ b/ephemeral/schema/bool_attribute.go @@ -0,0 +1,185 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go new file mode 100644 index 000000000..077ee1f1f --- /dev/null +++ b/ephemeral/schema/bool_attribute_test.go @@ -0,0 +1,425 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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) + } + }) + } +} diff --git a/ephemeral/schema/doc.go b/ephemeral/schema/doc.go new file mode 100644 index 000000000..93c0835e2 --- /dev/null +++ b/ephemeral/schema/doc.go @@ -0,0 +1,11 @@ +// 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. +// +// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until +// these notices are removed. +package schema diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go new file mode 100644 index 000000000..5088fef63 --- /dev/null +++ b/ephemeral/schema/dynamic_attribute.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} + +// 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..a718167a6 --- /dev/null +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -0,0 +1,425 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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..93ce2ac7d --- /dev/null +++ b/ephemeral/schema/float32_attribute.go @@ -0,0 +1,189 @@ +// 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 +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go new file mode 100644 index 000000000..e9e45d785 --- /dev/null +++ b/ephemeral/schema/float32_attribute_test.go @@ -0,0 +1,426 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go new file mode 100644 index 000000000..ff1912d1e --- /dev/null +++ b/ephemeral/schema/float64_attribute.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go new file mode 100644 index 000000000..4c3f2703a --- /dev/null +++ b/ephemeral/schema/float64_attribute_test.go @@ -0,0 +1,425 @@ +// 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go new file mode 100644 index 000000000..f98cbe3b0 --- /dev/null +++ b/ephemeral/schema/int32_attribute.go @@ -0,0 +1,189 @@ +// 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 +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go new file mode 100644 index 000000000..bd6d4d36c --- /dev/null +++ b/ephemeral/schema/int32_attribute_test.go @@ -0,0 +1,426 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go new file mode 100644 index 000000000..52b1b7ffd --- /dev/null +++ b/ephemeral/schema/int64_attribute.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go new file mode 100644 index 000000000..e61165c5f --- /dev/null +++ b/ephemeral/schema/int64_attribute_test.go @@ -0,0 +1,425 @@ +// 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go new file mode 100644 index 000000000..3846316b6 --- /dev/null +++ b/ephemeral/schema/list_attribute.go @@ -0,0 +1,220 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "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 ( + _ 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 + // 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 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 +} + +// 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..1b22bbbdc --- /dev/null +++ b/ephemeral/schema/list_attribute_test.go @@ -0,0 +1,523 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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_attribute.go b/ephemeral/schema/list_nested_attribute.go new file mode 100644 index 000000000..4a91f2742 --- /dev/null +++ b/ephemeral/schema/list_nested_attribute.go @@ -0,0 +1,244 @@ +// 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 ( + _ 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 +} + +// 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..3d1ae651d --- /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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..bb20c35de --- /dev/null +++ b/ephemeral/schema/list_nested_block_test.go @@ -0,0 +1,570 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..366619ed5 --- /dev/null +++ b/ephemeral/schema/map_attribute.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "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 ( + _ 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 +} + +// 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..94219da83 --- /dev/null +++ b/ephemeral/schema/map_attribute_test.go @@ -0,0 +1,523 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..11d701cc9 --- /dev/null +++ b/ephemeral/schema/map_nested_attribute.go @@ -0,0 +1,245 @@ +// 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 +} + +// 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..0e7986f89 --- /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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..65c76544b --- /dev/null +++ b/ephemeral/schema/nested_attribute_object_test.go @@ -0,0 +1,280 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..f484d10ca --- /dev/null +++ b/ephemeral/schema/nested_block_object_test.go @@ -0,0 +1,366 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..547396409 --- /dev/null +++ b/ephemeral/schema/number_attribute.go @@ -0,0 +1,189 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} + +// 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..7e326b90e --- /dev/null +++ b/ephemeral/schema/number_attribute_test.go @@ -0,0 +1,425 @@ +// 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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..1ff506f9d --- /dev/null +++ b/ephemeral/schema/object_attribute.go @@ -0,0 +1,222 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "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 ( + _ 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 + // 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 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 +} + +// 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..429aba1e7 --- /dev/null +++ b/ephemeral/schema/object_attribute_test.go @@ -0,0 +1,556 @@ +// 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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..64cc4806b --- /dev/null +++ b/ephemeral/schema/schema_test.go @@ -0,0 +1,1357 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, tc := name, tc + + 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 { + name, tc := name, tc + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..7f6a408ed --- /dev/null +++ b/ephemeral/schema/set_attribute.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "context" + + "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 ( + _ 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 +} + +// 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..0b6903829 --- /dev/null +++ b/ephemeral/schema/set_attribute_test.go @@ -0,0 +1,523 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..7ed6d2fdf --- /dev/null +++ b/ephemeral/schema/set_nested_attribute.go @@ -0,0 +1,240 @@ +// 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 +} + +// 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..1bd3daa65 --- /dev/null +++ b/ephemeral/schema/set_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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..0862026f4 --- /dev/null +++ b/ephemeral/schema/set_nested_block_test.go @@ -0,0 +1,570 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..a57db9c65 --- /dev/null +++ b/ephemeral/schema/single_nested_attribute.go @@ -0,0 +1,244 @@ +// 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 +} + +// 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..5b7209edb --- /dev/null +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -0,0 +1,569 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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..994f94894 --- /dev/null +++ b/ephemeral/schema/single_nested_block_test.go @@ -0,0 +1,485 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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..1b5c7db22 --- /dev/null +++ b/ephemeral/schema/string_attribute.go @@ -0,0 +1,185 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "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 ( + _ 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 +} + +// 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..1c95add7d --- /dev/null +++ b/ephemeral/schema/string_attribute_test.go @@ -0,0 +1,425 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 { + name, testCase := name, testCase + + 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 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 { + name, testCase := name, testCase + + 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..ace26d48e --- /dev/null +++ b/ephemeral/validate_config.go @@ -0,0 +1,32 @@ +// 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/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 3a6347dc4..0eaf40c50 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,16 @@ 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, + } +} 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..e9ba362ae --- /dev/null +++ b/internal/fromproto5/closeephemeralresource_test.go @@ -0,0 +1,101 @@ +// 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 { + name, testCase := name, testCase + + 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/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..40f2ebff5 --- /dev/null +++ b/internal/fromproto5/ephemeral_result_data_test.go @@ -0,0 +1,122 @@ +// 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 { + name, testCase := name, testCase + + 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/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..bd53e32d7 --- /dev/null +++ b/internal/fromproto5/openephemeralresource_test.go @@ -0,0 +1,146 @@ +// 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 { + name, testCase := name, testCase + + 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/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..b61aed2ad --- /dev/null +++ b/internal/fromproto5/renewephemeralresource_test.go @@ -0,0 +1,101 @@ +// 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 { + name, testCase := name, testCase + + 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/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..bf4aec660 --- /dev/null +++ b/internal/fromproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,110 @@ +// 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 { + name, testCase := name, testCase + + 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/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index 6742a0303..cd9c92b9c 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,16 @@ 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, + } +} 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..dca9046c7 --- /dev/null +++ b/internal/fromproto6/closeephemeralresource_test.go @@ -0,0 +1,101 @@ +// 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 { + name, testCase := name, testCase + + 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/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..44396ee59 --- /dev/null +++ b/internal/fromproto6/ephemeral_result_data_test.go @@ -0,0 +1,122 @@ +// 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 { + name, testCase := name, testCase + + 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/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..beea040e4 --- /dev/null +++ b/internal/fromproto6/openephemeralresource_test.go @@ -0,0 +1,146 @@ +// 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 { + name, testCase := name, testCase + + 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/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..02779157f --- /dev/null +++ b/internal/fromproto6/renewephemeralresource_test.go @@ -0,0 +1,101 @@ +// 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 { + name, testCase := name, testCase + + 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/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..39d793335 --- /dev/null +++ b/internal/fromproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,110 @@ +// 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 { + name, testCase := name, testCase + + 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/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/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_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..4bad0e620 --- /dev/null +++ b/internal/fwserver/server_closeephemeralresource_test.go @@ -0,0 +1,206 @@ +// 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 { + name, testCase := name, testCase + + 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..f8dd1e9e0 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{ @@ -235,6 +249,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_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_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..532765e1a 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", @@ -488,6 +660,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..43a0c9a4e 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{ 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..ec2649430 --- /dev/null +++ b/internal/fwserver/server_openephemeralresource_test.go @@ -0,0 +1,402 @@ +// 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 { + name, testCase := name, testCase + + 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_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..368a5aa87 --- /dev/null +++ b/internal/fwserver/server_renewephemeralresource_test.go @@ -0,0 +1,283 @@ +// 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 { + name, testCase := name, testCase + + 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_validateephemeralresourceconfig.go b/internal/fwserver/server_validateephemeralresourceconfig.go new file mode 100644 index 000000000..a99a0dbfb --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -0,0 +1,109 @@ +// 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/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...) + } + + validateSchemaReq := ValidateSchemaRequest{ + 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..24cb08e70 --- /dev/null +++ b/internal/fwserver/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,308 @@ +// 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 { + name, testCase := name, testCase + + 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/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/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..46d987d1d --- /dev/null +++ b/internal/proto5server/server_closeephemeralresource_test.go @@ -0,0 +1,131 @@ +// 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 { + name, testCase := name, testCase + + 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_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index fffb8146f..39d2985bd 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,8 +62,9 @@ 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, @@ -97,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, @@ -136,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, @@ -154,6 +158,137 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, + "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, + }, + }, + }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -181,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", @@ -225,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, @@ -264,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, @@ -309,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", @@ -353,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, @@ -392,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, @@ -429,6 +570,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 59c34a40d..41a632391 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,7 +105,8 @@ 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{}, }, @@ -160,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, @@ -202,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, @@ -223,6 +228,198 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "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, + }, + }, + }, "functions": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -260,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{}, @@ -323,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, @@ -365,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, @@ -404,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{ @@ -444,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{}, }, @@ -513,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{}, }, @@ -594,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, @@ -636,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, @@ -785,6 +990,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_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..a4e7d2113 --- /dev/null +++ b/internal/proto5server/server_openephemeralresource_test.go @@ -0,0 +1,268 @@ +// 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 { + name, testCase := name, testCase + + 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_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..73a63f43b --- /dev/null +++ b/internal/proto5server/server_renewephemeralresource_test.go @@ -0,0 +1,165 @@ +// 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 { + name, testCase := name, testCase + + 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_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..6505bbf53 --- /dev/null +++ b/internal/proto5server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,168 @@ +// 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 { + name, testCase := name, testCase + + 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_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..f1c732bcc --- /dev/null +++ b/internal/proto6server/server_closeephemeralresource_test.go @@ -0,0 +1,131 @@ +// 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 { + name, testCase := name, testCase + + 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_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 1029dd232..5a2395f7e 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,8 +62,9 @@ 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, @@ -107,8 +109,9 @@ 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, @@ -145,6 +148,138 @@ 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{ @@ -181,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", @@ -225,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, @@ -264,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, @@ -309,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", @@ -353,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, @@ -392,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, @@ -429,6 +570,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 2a0fd9753..24337c998 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,7 +105,8 @@ 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{}, }, @@ -170,7 +173,8 @@ 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{}, }, @@ -211,6 +215,199 @@ 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{}, + }, + 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{}, @@ -260,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{}, @@ -323,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, @@ -365,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, @@ -404,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{ @@ -444,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{}, }, @@ -513,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{}, }, @@ -594,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, @@ -636,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, @@ -785,6 +990,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_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..18167e499 --- /dev/null +++ b/internal/proto6server/server_openephemeralresource_test.go @@ -0,0 +1,268 @@ +// 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 { + name, testCase := name, testCase + + 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_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..2dfe3905b --- /dev/null +++ b/internal/proto6server/server_renewephemeralresource_test.go @@ -0,0 +1,165 @@ +// 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 { + name, testCase := name, testCase + + 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_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..9e1932143 --- /dev/null +++ b/internal/proto6server/server_validateephemeralresourceconfig_test.go @@ -0,0 +1,168 @@ +// 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 { + name, testCase := name, testCase + + 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/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/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..f10ab23b1 --- /dev/null +++ b/internal/toproto5/closeephemeralresource_test.go @@ -0,0 +1,69 @@ +// 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 { + name, testCase := name, testCase + + 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/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/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..bcb082b21 --- /dev/null +++ b/internal/toproto5/ephemeral_result_data_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func 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 { + name, testCase := name, testCase + + 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..2e3e13731 --- /dev/null +++ b/internal/toproto5/ephemeralresourcemetadata_test.go @@ -0,0 +1,46 @@ +// 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 { + name, testCase := name, testCase + + 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/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..f328d387e 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, 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..7a6cd4761 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,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-bool": { @@ -296,8 +304,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 +335,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 +366,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 +397,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,16 +428,1077 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + 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.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListNestedAttribute{ + 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-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + 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.List{ + 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-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + 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.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "data-source-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapNestedAttribute{ + 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-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-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{}, }, }, - "data-source-attribute-type-list-list-string": { + "ephemeral-resource-attribute-type-list-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + 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, @@ -436,8 +1509,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{ { @@ -457,15 +1531,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-nested-attributes": { + "ephemeral-resource-attribute-type-list-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListNestedAttribute{ - 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.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -477,26 +1551,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-list-object": { + "ephemeral-resource-attribute-type-list-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + 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{ @@ -509,8 +1584,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{ { @@ -532,12 +1608,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-list-string": { + "ephemeral-resource-attribute-type-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.StringType, }, @@ -546,8 +1622,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{ { @@ -565,15 +1642,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, - "data-source-attribute-type-map-nested-attributes": { + "ephemeral-resource-attribute-type-map-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapNestedAttribute{ - 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.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -585,26 +1662,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-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 +1691,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 +1711,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 +1724,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 +1742,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 +1758,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 +1780,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 +1800,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 +1833,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 +1857,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 +1873,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 +1895,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 +1909,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 +1929,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 +1947,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 +1975,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 +1993,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 +2006,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 +2024,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 +2043,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 +2069,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 +2088,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 +2114,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 +2131,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 +2169,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 +2198,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 +2222,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 +2250,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 +2282,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 +2305,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 +2329,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 +2357,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 +2386,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 +2414,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 +2443,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{ @@ -1370,8 +2472,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 +2500,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 +2528,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 +2556,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 +2584,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 +2615,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 +2654,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 +2683,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 +2718,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 +2755,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 +2780,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 +2810,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 +2841,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 +2880,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 +2909,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 +2946,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 +2979,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 +3014,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 +3038,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 +3066,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 +3100,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 +3142,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 +3182,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 +3218,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 +3246,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 +3274,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 +3302,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 +3330,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 +3361,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 +3400,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 +3429,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 +3464,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 +3501,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 +3526,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 +3556,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 +3587,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 +3626,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 +3655,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 +3692,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 +3725,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 +3760,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 +3784,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 +3821,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 +3863,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 +3895,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 +3927,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 +3959,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 +3991,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 +4023,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{ @@ -2904,8 +4055,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 +4086,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 +4117,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 +4148,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 +4179,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 +4213,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 +4255,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 +4288,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 +4326,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 +4366,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 +4395,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 +4428,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 +4462,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 +4504,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 +4537,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 +4577,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 +4613,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 +4651,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 +4679,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 +4710,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 +4747,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 +4792,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 +4835,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 +4870,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{}, 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..582a1f90e --- /dev/null +++ b/internal/toproto5/openephemeralresource_test.go @@ -0,0 +1,214 @@ +// 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 { + name, testCase := name, testCase + + 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/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..975e297c8 --- /dev/null +++ b/internal/toproto5/renewephemeralresource_test.go @@ -0,0 +1,117 @@ +// 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 { + name, testCase := name, testCase + + 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/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..0a2a939f3 --- /dev/null +++ b/internal/toproto5/validateephemeralresourceconfig_test.go @@ -0,0 +1,69 @@ +// 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 { + name, testCase := name, testCase + + 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/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..cf9830dd0 --- /dev/null +++ b/internal/toproto6/closeephemeralresource_test.go @@ -0,0 +1,69 @@ +// 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 { + name, testCase := name, testCase + + 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/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/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..18cb2aced --- /dev/null +++ b/internal/toproto6/ephemeral_result_data_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func 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 { + name, testCase := name, testCase + + 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..c62b90797 --- /dev/null +++ b/internal/toproto6/ephemeralresourcemetadata_test.go @@ -0,0 +1,46 @@ +// 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 { + name, testCase := name, testCase + + 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/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..40a6b05e7 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, 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..2df173187 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,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-bool": { @@ -296,8 +304,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 +335,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": { @@ -343,8 +353,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", + Required: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-int64": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.Int64Attribute{ + 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-list-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + Required: true, + ElementType: types.ListType{ + 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.List{ + ElementType: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-list-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListNestedAttribute{ + 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.SchemaObjectNestingModeList, + 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-list-object": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + 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.List{ + 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-list-string": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.ListAttribute{ + 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.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "data-source-attribute-type-map-nested-attributes": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.MapNestedAttribute{ + 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.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{ { @@ -360,12 +1479,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-int64": { + "ephemeral-resource-attribute-type-int64": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.Int64Attribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.Int64Attribute{ Required: true, }, }, @@ -373,8 +1492,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{ { @@ -390,12 +1510,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-list-string": { + "ephemeral-resource-attribute-type-list-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + 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, @@ -406,8 +1526,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{ { @@ -427,15 +1548,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-nested-attributes": { + "ephemeral-resource-attribute-type-list-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListNestedAttribute{ - 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.ListNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -447,8 +1568,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{ { @@ -473,12 +1595,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-object": { + "ephemeral-resource-attribute-type-list-object": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + 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{ @@ -491,8 +1613,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{ { @@ -514,12 +1637,12 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-list-string": { + "ephemeral-resource-attribute-type-list-string": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.ListAttribute{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.ListAttribute{ Required: true, ElementType: types.StringType, }, @@ -528,8 +1651,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{ { @@ -547,15 +1671,15 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, - "data-source-attribute-type-map-nested-attributes": { + "ephemeral-resource-attribute-type-map-nested-attributes": { input: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{ - "test_data_source": datasourceschema.Schema{ - Attributes: map[string]datasourceschema.Attribute{ - "test_attribute": datasourceschema.MapNestedAttribute{ - 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.MapNestedAttribute{ + NestedObject: ephemeralschema.NestedAttributeObject{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_nested_attribute": ephemeralschema.StringAttribute{ Required: true, }, }, @@ -567,8 +1691,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{ { @@ -593,12 +1718,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 +1732,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 +1752,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 +1765,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 +1783,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 +1799,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 +1821,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 +1841,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 +1868,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 +1886,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 +1910,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 +1926,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 +1948,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 +1962,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 +1982,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 +2000,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 +2027,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 +2040,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 +2058,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 +2071,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 +2089,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 +2108,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 +2134,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 +2153,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 +2179,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 +2196,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 +2234,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 +2263,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 +2287,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 +2315,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 +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{}, @@ -1227,7 +2370,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 +2394,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 +2422,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 +2451,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 +2479,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 +2508,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{ @@ -1388,8 +2537,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 +2565,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 +2593,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 +2621,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 +2652,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 +2691,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 +2733,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 +2768,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 +2805,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 +2843,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 +2873,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 +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{ @@ -1782,8 +2943,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 +2985,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 +3022,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 +3055,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 +3090,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 +3127,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 +3155,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 +3189,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 +3231,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 +3271,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 +3307,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 +3335,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 +3363,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 +3391,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 +3419,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 +3450,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 +3489,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 +3531,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 +3566,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 +3603,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 +3641,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 +3671,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 +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{ @@ -2557,8 +3741,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 +3783,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 +3820,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 +3853,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 +3888,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 +3925,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 +3962,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 +4004,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 +4036,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 +4068,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 +4100,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 +4132,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 +4164,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{ @@ -2999,8 +4196,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 +4227,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 +4258,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 +4289,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 +4323,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 +4365,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 +4410,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 +4448,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 +4488,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 +4529,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 +4562,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 +4596,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 +4638,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 +4683,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 +4723,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 +4759,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 +4797,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 +4837,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 +4868,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 +4905,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 +4950,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 +4993,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 +5028,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{}, 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..734294018 --- /dev/null +++ b/internal/toproto6/openephemeralresource_test.go @@ -0,0 +1,214 @@ +// 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 { + name, testCase := name, testCase + + 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/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..54c3b4f64 --- /dev/null +++ b/internal/toproto6/renewephemeralresource_test.go @@ -0,0 +1,117 @@ +// 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 { + name, testCase := name, testCase + + 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/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..a088f2b88 --- /dev/null +++ b/internal/toproto6/validateephemeralresourceconfig_test.go @@ -0,0 +1,69 @@ +// 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 { + name, testCase := name, testCase + + 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/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/provider.go b/provider/provider.go index ff0d18e81..ba18927cb 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,24 @@ 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. +// +// NOTE: Ephemeral resource support is experimental and exposed without compatibility promises until +// these notices are removed. +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/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..d1cbc5a1a --- /dev/null +++ b/tfsdk/ephemeral_result_data_test.go @@ -0,0 +1,487 @@ +// 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 { + name, testCase := name, testCase + + 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 { + name, tc := name, tc + 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 { + name, testCase := name, testCase + + 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 { + name, tc := name, tc + 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 { + name, tc := name, tc + 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/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index b1fb0c9a6..2297fad3c 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -265,6 +265,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/ephemeral-resources/close.mdx b/website/docs/plugin/framework/ephemeral-resources/close.mdx new file mode 100644 index 000000000..7781c187b --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/close.mdx @@ -0,0 +1,94 @@ +--- +page_title: 'Plugin Development - Framework: Open Ephemeral Resources' +description: >- + How to implement ephemeral resource close in the provider development framework. +--- + +# Close 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..d0616cd6a --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/configure.mdx @@ -0,0 +1,103 @@ +--- +page_title: 'Plugin Development - Framework: Configure Ephemeral Resources' +description: >- + How to configure ephemeral resources with provider data or clients in the provider development framework. +--- + +# Configure 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..e3cce7129 --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/index.mdx @@ -0,0 +1,101 @@ +--- +page_title: 'Plugin Development - Framework: Ephemeral Resources' +description: >- + How to build ephemeral resources in the provider development framework. Ephemeral + resources allow Terraform to reference external data, while guaranteeing that this + data will not be persisted in plan or state. +--- + +# 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/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/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..d0636d4b4 --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/open.mdx @@ -0,0 +1,76 @@ +--- +page_title: 'Plugin Development - Framework: Open Ephemeral Resources' +description: >- + How to implement ephemeral resource open in the provider development framework. +--- + +# Open 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..4886cb767 --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/renew.mdx @@ -0,0 +1,113 @@ +--- +page_title: 'Plugin Development - Framework: Open Ephemeral Resources' +description: >- + How to implement ephemeral resource renew in the provider development framework. +--- + +# Renew 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..874216adc --- /dev/null +++ b/website/docs/plugin/framework/ephemeral-resources/validate-configuration.mdx @@ -0,0 +1,85 @@ +--- +page_title: 'Plugin Development - Framework: Validate Ephemeral Resource Configurations' +description: >- + How to validate ephemeral resource configurations with the provider development framework. +--- + +# Validate Configuration + +[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/handling-data/attributes/bool.mdx b/website/docs/plugin/framework/handling-data/attributes/bool.mdx index 48ffb70a9..2d37ffcd1 100644 --- a/website/docs/plugin/framework/handling-data/attributes/bool.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/bool.mdx @@ -25,6 +25,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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx index f399ce665..4c95a01d6 100644 --- a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx @@ -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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/float32.mdx b/website/docs/plugin/framework/handling-data/attributes/float32.mdx index 0d794db90..be23a0fc0 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -31,6 +31,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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/float64.mdx b/website/docs/plugin/framework/handling-data/attributes/float64.mdx index b8364f462..9e062a18c 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -31,6 +31,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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx index ad4b46a8f..0feb915bb 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -31,6 +31,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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/int64.mdx b/website/docs/plugin/framework/handling-data/attributes/int64.mdx index 5d793a9cf..7e1e004a8 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -31,6 +31,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`: 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..f088d5e35 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx @@ -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. diff --git a/website/docs/plugin/framework/handling-data/attributes/list.mdx b/website/docs/plugin/framework/handling-data/attributes/list.mdx index 159966f9b..382696c35 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list.mdx @@ -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. 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..7b2c52d3c 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx @@ -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. diff --git a/website/docs/plugin/framework/handling-data/attributes/map.mdx b/website/docs/plugin/framework/handling-data/attributes/map.mdx index 71a52b24b..368c4e25b 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map.mdx @@ -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. diff --git a/website/docs/plugin/framework/handling-data/attributes/number.mdx b/website/docs/plugin/framework/handling-data/attributes/number.mdx index 3f47e5d5a..1afe11e19 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -31,6 +31,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`: diff --git a/website/docs/plugin/framework/handling-data/attributes/object.mdx b/website/docs/plugin/framework/handling-data/attributes/object.mdx index 4af6dda0d..05be71d36 100644 --- a/website/docs/plugin/framework/handling-data/attributes/object.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/object.mdx @@ -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). 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..ca1ce3cce 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx @@ -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..29724eb3b 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set.mdx @@ -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..5b76dcaa9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx @@ -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. diff --git a/website/docs/plugin/framework/handling-data/attributes/string.mdx b/website/docs/plugin/framework/handling-data/attributes/string.mdx index cb02da57d..b3b49f80e 100644 --- a/website/docs/plugin/framework/handling-data/attributes/string.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/string.mdx @@ -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`: 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..ac98044de 100644 --- a/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/list-nested.mdx @@ -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.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..63d2cd8da 100644 --- a/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/set-nested.mdx @@ -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..f318cf6b6 100644 --- a/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/blocks/single-nested.mdx @@ -40,6 +40,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/schemas.mdx b/website/docs/plugin/framework/handling-data/schemas.mdx index a36661221..46863ab79 100644 --- a/website/docs/plugin/framework/handling-data/schemas.mdx +++ b/website/docs/plugin/framework/handling-data/schemas.mdx @@ -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..5931d62fe 100644 --- a/website/docs/plugin/framework/handling-data/terraform-concepts.mdx +++ b/website/docs/plugin/framework/handling-data/terraform-concepts.mdx @@ -10,7 +10,7 @@ This page describes Terraform concepts as they relate to handling data within fr ## 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 +62,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..99ce3137f 100644 --- a/website/docs/plugin/framework/handling-data/types/bool.mdx +++ b/website/docs/plugin/framework/handling-data/types/bool.mdx @@ -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/dynamic.mdx b/website/docs/plugin/framework/handling-data/types/dynamic.mdx index f647f3789..ad26bde6d 100644 --- a/website/docs/plugin/framework/handling-data/types/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/types/dynamic.mdx @@ -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..20208798d 100644 --- a/website/docs/plugin/framework/handling-data/types/float32.mdx +++ b/website/docs/plugin/framework/handling-data/types/float32.mdx @@ -25,6 +25,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..671f5db31 100644 --- a/website/docs/plugin/framework/handling-data/types/float64.mdx +++ b/website/docs/plugin/framework/handling-data/types/float64.mdx @@ -25,6 +25,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/int32.mdx b/website/docs/plugin/framework/handling-data/types/int32.mdx index 1a26debc4..7f3d9e0f3 100644 --- a/website/docs/plugin/framework/handling-data/types/int32.mdx +++ b/website/docs/plugin/framework/handling-data/types/int32.mdx @@ -25,6 +25,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..00b7ebabc 100644 --- a/website/docs/plugin/framework/handling-data/types/int64.mdx +++ b/website/docs/plugin/framework/handling-data/types/int64.mdx @@ -25,6 +25,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..4e53a5661 100644 --- a/website/docs/plugin/framework/handling-data/types/list.mdx +++ b/website/docs/plugin/framework/handling-data/types/list.mdx @@ -19,6 +19,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 +31,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..83b618f64 100644 --- a/website/docs/plugin/framework/handling-data/types/map.mdx +++ b/website/docs/plugin/framework/handling-data/types/map.mdx @@ -19,6 +19,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 +27,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..eccf72643 100644 --- a/website/docs/plugin/framework/handling-data/types/number.mdx +++ b/website/docs/plugin/framework/handling-data/types/number.mdx @@ -25,6 +25,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..2c6cdad7c 100644 --- a/website/docs/plugin/framework/handling-data/types/object.mdx +++ b/website/docs/plugin/framework/handling-data/types/object.mdx @@ -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..51eef5ee6 100644 --- a/website/docs/plugin/framework/handling-data/types/set.mdx +++ b/website/docs/plugin/framework/handling-data/types/set.mdx @@ -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..d788dbb76 100644 --- a/website/docs/plugin/framework/handling-data/types/string.mdx +++ b/website/docs/plugin/framework/handling-data/types/string.mdx @@ -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..079d879cb 100644 --- a/website/docs/plugin/framework/handling-data/types/tuple.mdx +++ b/website/docs/plugin/framework/handling-data/types/tuple.mdx @@ -18,7 +18,7 @@ The tuple type is used to express Terraform's [tuple type constraint](/terraform ## 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/providers/index.mdx b/website/docs/plugin/framework/providers/index.mdx index dc3352c33..915cfc49f 100644 --- a/website/docs/plugin/framework/providers/index.mdx +++ b/website/docs/plugin/framework/providers/index.mdx @@ -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/validation.mdx b/website/docs/plugin/framework/validation.mdx index b0349fd8a..2711dc65b 100644 --- a/website/docs/plugin/framework/validation.mdx +++ b/website/docs/plugin/framework/validation.mdx @@ -12,10 +12,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 From 497e13a78bced22839ffba8bc687a5385591959a Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 31 Oct 2024 18:13:02 +0000 Subject: [PATCH 6/6] Update changelog --- .changes/1.13.0.md | 17 +++++++++++++++++ .../ENHANCEMENTS-20241028-130457.yaml | 6 ------ .../ENHANCEMENTS-20241028-130618.yaml | 6 ------ .../ENHANCEMENTS-20241028-130758.yaml | 6 ------ .../unreleased/FEATURES-20241028-130339.yaml | 5 ----- .../unreleased/FEATURES-20241028-130855.yaml | 5 ----- .changes/unreleased/NOTES-20241028-130308.yaml | 6 ------ CHANGELOG.md | 17 +++++++++++++++++ 8 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 .changes/1.13.0.md delete mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130457.yaml delete mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130618.yaml delete mode 100644 .changes/unreleased/ENHANCEMENTS-20241028-130758.yaml delete mode 100644 .changes/unreleased/FEATURES-20241028-130339.yaml delete mode 100644 .changes/unreleased/FEATURES-20241028-130855.yaml delete mode 100644 .changes/unreleased/NOTES-20241028-130308.yaml 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/unreleased/ENHANCEMENTS-20241028-130457.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130457.yaml deleted file mode 100644 index d477f1130..000000000 --- a/.changes/unreleased/ENHANCEMENTS-20241028-130457.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: ENHANCEMENTS -body: 'provider: Added `ProviderWithEphemeralResources` interface for implementing - ephemeral resources' -time: 2024-10-28T13:04:57.796703-04:00 -custom: - Issue: "1050" diff --git a/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml deleted file mode 100644 index f52d2d14e..000000000 --- a/.changes/unreleased/ENHANCEMENTS-20241028-130618.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: ENHANCEMENTS -body: 'tfsdk: Added `EphemeralResultData` struct for representing ephemeral values - produced by a provider, such as from an ephemeral resource' -time: 2024-10-28T13:06:18.799164-04:00 -custom: - Issue: "1050" diff --git a/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml b/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml deleted file mode 100644 index 720293494..000000000 --- a/.changes/unreleased/ENHANCEMENTS-20241028-130758.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: ENHANCEMENTS -body: 'provider: Added `EphemeralResourceData` to `ConfigureResponse`, to pass provider-defined - data to `ephemeral.EphemeralResource` implementations' -time: 2024-10-28T13:07:58.9914-04:00 -custom: - Issue: "1050" diff --git a/.changes/unreleased/FEATURES-20241028-130339.yaml b/.changes/unreleased/FEATURES-20241028-130339.yaml deleted file mode 100644 index 04c58e75d..000000000 --- a/.changes/unreleased/FEATURES-20241028-130339.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'ephemeral: New package for implementing ephemeral resources' -time: 2024-10-28T13:03:39.23218-04:00 -custom: - Issue: "1050" diff --git a/.changes/unreleased/FEATURES-20241028-130855.yaml b/.changes/unreleased/FEATURES-20241028-130855.yaml deleted file mode 100644 index 57d93a69d..000000000 --- a/.changes/unreleased/FEATURES-20241028-130855.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'ephemeral/schema: New package for implementing ephemeral resource schemas' -time: 2024-10-28T13:08:55.520004-04:00 -custom: - Issue: "1050" diff --git a/.changes/unreleased/NOTES-20241028-130308.yaml b/.changes/unreleased/NOTES-20241028-130308.yaml deleted file mode 100644 index 5c9ca5240..000000000 --- a/.changes/unreleased/NOTES-20241028-130308.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: NOTES -body: Ephemeral resource support is in technical preview and offered without compatibility - promises until Terraform 1.10 is generally available. -time: 2024-10-28T13:03:08.373897-04:00 -custom: - Issue: "1050" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6816c4e29..67f78f7fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 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: