From 9765f809ba830df13c6660fa726f11b5dc329150 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:49:15 -0400 Subject: [PATCH 1/5] Result of tsccr-helper -log-level=info gha update -latest . (#240) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-changie.yml | 2 +- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 6 +++--- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/compliance.yml | 2 +- .github/workflows/release.yml | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml index 60a514c0..33ba8ee8 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 2358e4ad..47f1bbb6 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 60928e9b..813d4dbb 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' @@ -30,14 +30,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 d978baa4..9e5193bc 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 ddd4d72a..9511923d 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e253c38d..4b3613dc 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 371aa75bf61eca441f8dcd5d8b81898ece8dde85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:39:32 -0400 Subject: [PATCH 2/5] build(deps): bump github.com/hashicorp/terraform-plugin-go (#241) 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 | 6 +++--- go.sum | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 1ebd9191..d7cc71f3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 github.com/hashicorp/terraform-plugin-framework v1.12.0 - github.com/hashicorp/terraform-plugin-go v0.24.0 + github.com/hashicorp/terraform-plugin-go v0.25.0 ) require ( @@ -15,9 +15,9 @@ require ( github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // 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/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/sys v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 3521a4c7..c502c33e 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,17 @@ github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+ github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ= github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE= -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/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -35,8 +36,9 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w 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/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= 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 761f545adef61af9c75d1a7fe1eb6337c7f3ba63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:41:10 -0400 Subject: [PATCH 3/5] build(deps): bump github.com/hashicorp/terraform-plugin-framework (#243) Bumps [github.com/hashicorp/terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) from 1.12.0 to 1.13.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-framework/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-framework/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-framework/compare/v1.12.0...v1.13.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-framework 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 | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d7cc71f3..75219a0c 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-framework v1.12.0 + github.com/hashicorp/terraform-plugin-framework v1.13.0 github.com/hashicorp/terraform-plugin-go v0.25.0 ) diff --git a/go.sum b/go.sum index c502c33e..fc181b56 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ 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/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ= -github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE= +github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= +github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= 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= From b793fd3dbc6e307a1e1129cd0da3cfd211b664ff Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 31 Oct 2024 14:46:37 -0400 Subject: [PATCH 4/5] ephemeralvalidator: Introduce new package for common ephemeral resource configuration validators (#242) * use WIP framework branch * implementation of the shared config validators for ephemeral resources * update go mod * go mod tidy * changelog --- .../unreleased/FEATURES-20241030-164618.yaml | 6 + ephemeralvalidator/all.go | 57 +++++ ephemeralvalidator/all_example_test.go | 21 ++ ephemeralvalidator/all_test.go | 178 +++++++++++++++ ephemeralvalidator/any.go | 65 ++++++ ephemeralvalidator/any_example_test.go | 17 ++ ephemeralvalidator/any_test.go | 155 +++++++++++++ ephemeralvalidator/any_with_all_warnings.go | 67 ++++++ .../any_with_all_warnings_example_test.go | 17 ++ .../any_with_all_warnings_test.go | 216 ++++++++++++++++++ ephemeralvalidator/at_least_one_of.go | 18 ++ .../at_least_one_of_example_test.go | 22 ++ ephemeralvalidator/at_least_one_of_test.go | 123 ++++++++++ ephemeralvalidator/conflicting.go | 18 ++ .../conflicting_example_test.go | 22 ++ ephemeralvalidator/conflicting_test.go | 124 ++++++++++ ephemeralvalidator/doc.go | 13 ++ ephemeralvalidator/exactly_one_of.go | 18 ++ .../exactly_one_of_example_test.go | 22 ++ ephemeralvalidator/exactly_one_of_test.go | 124 ++++++++++ ephemeralvalidator/required_together.go | 18 ++ .../required_together_example_test.go | 22 ++ ephemeralvalidator/required_together_test.go | 124 ++++++++++ internal/configvalidator/at_least_one_of.go | 5 + .../configvalidator/at_least_one_of_test.go | 109 +++++++++ internal/configvalidator/conflicting.go | 5 + internal/configvalidator/conflicting_test.go | 110 +++++++++ internal/configvalidator/doc.go | 4 +- internal/configvalidator/exactly_one_of.go | 5 + .../configvalidator/exactly_one_of_test.go | 110 +++++++++ internal/configvalidator/required_together.go | 5 + .../configvalidator/required_together_test.go | 110 +++++++++ internal/testvalidator/warning.go | 13 ++ 33 files changed, 1941 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/FEATURES-20241030-164618.yaml create mode 100644 ephemeralvalidator/all.go create mode 100644 ephemeralvalidator/all_example_test.go create mode 100644 ephemeralvalidator/all_test.go create mode 100644 ephemeralvalidator/any.go create mode 100644 ephemeralvalidator/any_example_test.go create mode 100644 ephemeralvalidator/any_test.go create mode 100644 ephemeralvalidator/any_with_all_warnings.go create mode 100644 ephemeralvalidator/any_with_all_warnings_example_test.go create mode 100644 ephemeralvalidator/any_with_all_warnings_test.go create mode 100644 ephemeralvalidator/at_least_one_of.go create mode 100644 ephemeralvalidator/at_least_one_of_example_test.go create mode 100644 ephemeralvalidator/at_least_one_of_test.go create mode 100644 ephemeralvalidator/conflicting.go create mode 100644 ephemeralvalidator/conflicting_example_test.go create mode 100644 ephemeralvalidator/conflicting_test.go create mode 100644 ephemeralvalidator/doc.go create mode 100644 ephemeralvalidator/exactly_one_of.go create mode 100644 ephemeralvalidator/exactly_one_of_example_test.go create mode 100644 ephemeralvalidator/exactly_one_of_test.go create mode 100644 ephemeralvalidator/required_together.go create mode 100644 ephemeralvalidator/required_together_example_test.go create mode 100644 ephemeralvalidator/required_together_test.go diff --git a/.changes/unreleased/FEATURES-20241030-164618.yaml b/.changes/unreleased/FEATURES-20241030-164618.yaml new file mode 100644 index 00000000..957c73cc --- /dev/null +++ b/.changes/unreleased/FEATURES-20241030-164618.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'ephemeralvalidator: Introduce new package with declarative validators for ephemeral + resource configurations' +time: 2024-10-30T16:46:18.935223-04:00 +custom: + Issue: "242" diff --git a/ephemeralvalidator/all.go b/ephemeralvalidator/all.go new file mode 100644 index 00000000..7ee46787 --- /dev/null +++ b/ephemeralvalidator/all.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator { + return allValidator{ + validators: validators, + } +} + +var _ ephemeral.ConfigValidator = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []ephemeral.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateEphemeralResource performs the validation. +func (v allValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &ephemeral.ValidateConfigResponse{} + + subValidator.ValidateEphemeralResource(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/ephemeralvalidator/all_example_test.go b/ephemeralvalidator/all_example_test.go new file mode 100644 index 00000000..72f8c648 --- /dev/null +++ b/ephemeralvalidator/all_example_test.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" +) + +func ExampleAll() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + // The configuration must satisfy either All validator. + ephemeralvalidator.Any( + ephemeralvalidator.All( /* ... */ ), + ephemeralvalidator.All( /* ... */ ), + ), + } +} diff --git a/ephemeralvalidator/all_test.go b/ephemeralvalidator/all_test.go new file mode 100644 index 00000000..c8b158bf --- /dev/null +++ b/ephemeralvalidator/all_test.go @@ -0,0 +1,178 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" +) + +func TestAllValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []ephemeral.ConfigValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.All( + ephemeralvalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + ephemeralvalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.All( + ephemeralvalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + ephemeralvalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.WithPath(path.Root("test3"), + diag.NewErrorDiagnostic( + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test3,test5]", + )), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + ephemeralvalidator.Any(testCase.validators...).ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/any.go b/ephemeralvalidator/any.go new file mode 100644 index 00000000..4c3ea6e8 --- /dev/null +++ b/ephemeralvalidator/any.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +// Any returns a validator which ensures that any configured attribute value +// passes at least one of the given validators. +// +// To prevent practitioner confusion should non-passing validators have +// conflicting logic, only warnings from the passing validator are returned. +// Use AnyWithAllWarnings() to return warnings from non-passing validators +// as well. +func Any(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator { + return anyValidator{ + validators: validators, + } +} + +var _ ephemeral.ConfigValidator = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []ephemeral.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateEphemeralResource performs the validation. +func (v anyValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &ephemeral.ValidateConfigResponse{} + + subValidator.ValidateEphemeralResource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/ephemeralvalidator/any_example_test.go b/ephemeralvalidator/any_example_test.go new file mode 100644 index 00000000..30e98f1d --- /dev/null +++ b/ephemeralvalidator/any_example_test.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" +) + +func ExampleAny() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + ephemeralvalidator.Any( /* ... */ ), + } +} diff --git a/ephemeralvalidator/any_test.go b/ephemeralvalidator/any_test.go new file mode 100644 index 00000000..aa990286 --- /dev/null +++ b/ephemeralvalidator/any_test.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" +) + +func TestAnyValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []ephemeral.ConfigValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + ephemeralvalidator.Any(testCase.validators...).ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/any_with_all_warnings.go b/ephemeralvalidator/any_with_all_warnings.go new file mode 100644 index 00000000..f840d115 --- /dev/null +++ b/ephemeralvalidator/any_with_all_warnings.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" +) + +// AnyWithAllWarnings returns a validator which ensures that any configured +// attribute value passes at least one of the given validators. This validator +// returns all warnings, including failed validators. +// +// Use Any() to return warnings only from the passing validator. +func AnyWithAllWarnings(validators ...ephemeral.ConfigValidator) ephemeral.ConfigValidator { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ ephemeral.ConfigValidator = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []ephemeral.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateEphemeralResource performs the validation. +func (v anyWithAllWarningsValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &ephemeral.ValidateConfigResponse{} + + subValidator.ValidateEphemeralResource(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/ephemeralvalidator/any_with_all_warnings_example_test.go b/ephemeralvalidator/any_with_all_warnings_example_test.go new file mode 100644 index 00000000..83c05933 --- /dev/null +++ b/ephemeralvalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" +) + +func ExampleAnyWithAllWarnings() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + ephemeralvalidator.AnyWithAllWarnings( /* ... */ ), + } +} diff --git a/ephemeralvalidator/any_with_all_warnings_test.go b/ephemeralvalidator/any_with_all_warnings_test.go new file mode 100644 index 00000000..c57c7547 --- /dev/null +++ b/ephemeralvalidator/any_with_all_warnings_test.go @@ -0,0 +1,216 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" +) + +func TestAnyWithAllWarningsValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []ephemeral.ConfigValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "valid": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "valid with warning": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.All( + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + testvalidator.WarningEphemeralResource("failing warning summary", "failing warning details"), + ), + ephemeralvalidator.All( + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + testvalidator.WarningEphemeralResource("passing warning summary", "passing warning details"), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("failing warning summary", "failing warning details"), + diag.NewWarningDiagnostic("passing warning summary", "passing warning details"), + }, + }, + }, + "invalid": { + validators: []ephemeral.ConfigValidator{ + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + ephemeralvalidator.AnyWithAllWarnings(testCase.validators...).ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/at_least_one_of.go b/ephemeralvalidator/at_least_one_of.go new file mode 100644 index 00000000..8be3bc9a --- /dev/null +++ b/ephemeralvalidator/at_least_one_of.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// AtLeastOneOf checks that a set of path.Expression has at least one non-null +// or unknown value. +func AtLeastOneOf(expressions ...path.Expression) ephemeral.ConfigValidator { + return &configvalidator.AtLeastOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/ephemeralvalidator/at_least_one_of_example_test.go b/ephemeralvalidator/at_least_one_of_example_test.go new file mode 100644 index 00000000..d3eeb4f4 --- /dev/null +++ b/ephemeralvalidator/at_least_one_of_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleAtLeastOneOf() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + // Validate at least one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value. + ephemeralvalidator.AtLeastOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/ephemeralvalidator/at_least_one_of_test.go b/ephemeralvalidator/at_least_one_of_test.go new file mode 100644 index 00000000..1c40aa7c --- /dev/null +++ b/ephemeralvalidator/at_least_one_of_test.go @@ -0,0 +1,123 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastOneOf(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "At least one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := ephemeralvalidator.AtLeastOneOf(testCase.pathExpressions...) + got := &ephemeral.ValidateConfigResponse{} + + validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/conflicting.go b/ephemeralvalidator/conflicting.go new file mode 100644 index 00000000..8ac8eac9 --- /dev/null +++ b/ephemeralvalidator/conflicting.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Conflicting checks that a set of path.Expression, are not configured +// simultaneously. +func Conflicting(expressions ...path.Expression) ephemeral.ConfigValidator { + return &configvalidator.ConflictingValidator{ + PathExpressions: expressions, + } +} diff --git a/ephemeralvalidator/conflicting_example_test.go b/ephemeralvalidator/conflicting_example_test.go new file mode 100644 index 00000000..bb1c2ca8 --- /dev/null +++ b/ephemeralvalidator/conflicting_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleConflicting() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + // Validate that schema defined attributes named attr1 and attr2 are not + // both configured with known, non-null values. + ephemeralvalidator.Conflicting( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/ephemeralvalidator/conflicting_test.go b/ephemeralvalidator/conflicting_test.go new file mode 100644 index 00000000..b8044b18 --- /dev/null +++ b/ephemeralvalidator/conflicting_test.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestConflicting(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := ephemeralvalidator.Conflicting(testCase.pathExpressions...) + got := &ephemeral.ValidateConfigResponse{} + + validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/doc.go b/ephemeralvalidator/doc.go new file mode 100644 index 00000000..52fd596b --- /dev/null +++ b/ephemeralvalidator/doc.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package ephemeralvalidator provides validators to express relationships +// between multiple attributes of an ephemeral resource. For example, checking that +// multiple attributes are not configured at the same time. +// +// These validators are implemented outside the schema, which may be easier to +// implement in provider code generation situations or suit provider code +// preferences differently than those in the schemavalidator package. Those +// validators start on a starting attribute, where relationships can be +// expressed as absolute paths to others or relative to the starting attribute. +package ephemeralvalidator diff --git a/ephemeralvalidator/exactly_one_of.go b/ephemeralvalidator/exactly_one_of.go new file mode 100644 index 00000000..dfe2a8f8 --- /dev/null +++ b/ephemeralvalidator/exactly_one_of.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// ExactlyOneOf checks that a set of path.Expression does not have more than +// one known value. +func ExactlyOneOf(expressions ...path.Expression) ephemeral.ConfigValidator { + return &configvalidator.ExactlyOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/ephemeralvalidator/exactly_one_of_example_test.go b/ephemeralvalidator/exactly_one_of_example_test.go new file mode 100644 index 00000000..7581f183 --- /dev/null +++ b/ephemeralvalidator/exactly_one_of_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleExactlyOneOf() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + // Validate only one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value. + ephemeralvalidator.ExactlyOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/ephemeralvalidator/exactly_one_of_test.go b/ephemeralvalidator/exactly_one_of_test.go new file mode 100644 index 00000000..a205ec1f --- /dev/null +++ b/ephemeralvalidator/exactly_one_of_test.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestExactlyOneOf(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := ephemeralvalidator.ExactlyOneOf(testCase.pathExpressions...) + got := &ephemeral.ValidateConfigResponse{} + + validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeralvalidator/required_together.go b/ephemeralvalidator/required_together.go new file mode 100644 index 00000000..565f4c0c --- /dev/null +++ b/ephemeralvalidator/required_together.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// RequiredTogether checks that a set of path.Expression either has all known +// or all null values. +func RequiredTogether(expressions ...path.Expression) ephemeral.ConfigValidator { + return &configvalidator.RequiredTogetherValidator{ + PathExpressions: expressions, + } +} diff --git a/ephemeralvalidator/required_together_example_test.go b/ephemeralvalidator/required_together_example_test.go new file mode 100644 index 00000000..906ec1e4 --- /dev/null +++ b/ephemeralvalidator/required_together_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleRequiredTogether() { + // Used inside a ephemeral.EphemeralResource type ConfigValidators method + _ = []ephemeral.ConfigValidator{ + // Validate the schema defined attributes named attr1 and attr2 are either + // both null or both known values. + ephemeralvalidator.RequiredTogether( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/ephemeralvalidator/required_together_test.go b/ephemeralvalidator/required_together_test.go new file mode 100644 index 00000000..9dd514d2 --- /dev/null +++ b/ephemeralvalidator/required_together_test.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ephemeralvalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/ephemeralvalidator" + "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/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiredTogether(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes must be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := ephemeralvalidator.RequiredTogether(testCase.pathExpressions...) + got := &ephemeral.ValidateConfigResponse{} + + validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/configvalidator/at_least_one_of.go b/internal/configvalidator/at_least_one_of.go index 9d67ef4a..03a32a35 100644 --- a/internal/configvalidator/at_least_one_of.go +++ b/internal/configvalidator/at_least_one_of.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "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/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -45,6 +46,10 @@ func (v AtLeastOneOfValidator) ValidateResource(ctx context.Context, req resourc resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v AtLeastOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v AtLeastOneOfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, unknownPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/at_least_one_of_test.go b/internal/configvalidator/at_least_one_of_test.go index 9b583649..94eeb86f 100644 --- a/internal/configvalidator/at_least_one_of_test.go +++ b/internal/configvalidator/at_least_one_of_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/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -934,3 +935,111 @@ func TestAtLeastOneOfValidatorValidateResource(t *testing.T) { }) } } + +func TestAtLeastOneOfValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validator configvalidator.AtLeastOneOfValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validator: configvalidator.AtLeastOneOfValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validator: configvalidator.AtLeastOneOfValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "At least one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + testCase.validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/configvalidator/conflicting.go b/internal/configvalidator/conflicting.go index edd2abd8..38dfd5c4 100644 --- a/internal/configvalidator/conflicting.go +++ b/internal/configvalidator/conflicting.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "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/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -46,6 +47,10 @@ func (v ConflictingValidator) ValidateResource(ctx context.Context, req resource resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v ConflictingValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v ConflictingValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/conflicting_test.go b/internal/configvalidator/conflicting_test.go index d9b63495..1f513f27 100644 --- a/internal/configvalidator/conflicting_test.go +++ b/internal/configvalidator/conflicting_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/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -941,3 +942,112 @@ func TestConflictingValidatorValidateResource(t *testing.T) { }) } } + +func TestConflictingValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validator configvalidator.ConflictingValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validator: configvalidator.ConflictingValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validator: configvalidator.ConflictingValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + testCase.validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/configvalidator/doc.go b/internal/configvalidator/doc.go index 789b4159..b3533ae2 100644 --- a/internal/configvalidator/doc.go +++ b/internal/configvalidator/doc.go @@ -2,6 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 // Package configvalidator provides the generic configuration validator -// implementations for the exported datasourcevalidator, providervalidator, and -// resourcevalidator packages. +// implementations for the exported datasourcevalidator, providervalidator, +// resourcevalidator, and ephemeralvalidator packages. package configvalidator diff --git a/internal/configvalidator/exactly_one_of.go b/internal/configvalidator/exactly_one_of.go index b76786f8..14904d5a 100644 --- a/internal/configvalidator/exactly_one_of.go +++ b/internal/configvalidator/exactly_one_of.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "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/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -46,6 +47,10 @@ func (v ExactlyOneOfValidator) ValidateResource(ctx context.Context, req resourc resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v ExactlyOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v ExactlyOneOfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, unknownPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/exactly_one_of_test.go b/internal/configvalidator/exactly_one_of_test.go index fd85bf9b..805c2ed8 100644 --- a/internal/configvalidator/exactly_one_of_test.go +++ b/internal/configvalidator/exactly_one_of_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/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -961,3 +962,112 @@ func TestExactlyOneOfValidatorValidateResource(t *testing.T) { }) } } + +func TestExactlyOneOfValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validator configvalidator.ExactlyOneOfValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validator: configvalidator.ExactlyOneOfValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validator: configvalidator.ExactlyOneOfValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + testCase.validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/configvalidator/required_together.go b/internal/configvalidator/required_together.go index 69c46671..e694e91d 100644 --- a/internal/configvalidator/required_together.go +++ b/internal/configvalidator/required_together.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "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/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -46,6 +47,10 @@ func (v RequiredTogetherValidator) ValidateResource(ctx context.Context, req res resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v RequiredTogetherValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v RequiredTogetherValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, foundPaths, unknownPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/required_together_test.go b/internal/configvalidator/required_together_test.go index 7f7a1c9f..fd7f04f7 100644 --- a/internal/configvalidator/required_together_test.go +++ b/internal/configvalidator/required_together_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/diag" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -929,3 +930,112 @@ func TestRequiredTogetherValidatorValidateResource(t *testing.T) { }) } } + +func TestRequiredTogetherValidatorValidateEphemeralResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validator configvalidator.RequiredTogetherValidator + req ephemeral.ValidateConfigRequest + expected *ephemeral.ValidateConfigResponse + }{ + "no-diagnostics": { + validator: configvalidator.RequiredTogetherValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + 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, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{}, + }, + "diagnostics": { + validator: configvalidator.RequiredTogetherValidator{ + PathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + }, + req: ephemeral.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &ephemeral.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes must be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &ephemeral.ValidateConfigResponse{} + + testCase.validator.ValidateEphemeralResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index f92a14ac..f988ed99 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.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" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -108,6 +109,14 @@ func WarningResource(summary string, detail string) resource.ConfigValidator { } } +// WarningEphemeralResource returns a validator which returns a warning diagnostic. +func WarningEphemeralResource(summary string, detail string) ephemeral.ConfigValidator { + return WarningValidator{ + Summary: summary, + Detail: detail, + } +} + // WarningSet returns a validator which returns a warning diagnostic. func WarningSet(summary string, detail string) validator.Set { return WarningValidator{ @@ -202,6 +211,10 @@ func (v WarningValidator) ValidateResource(ctx context.Context, request resource response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateEphemeralResource(ctx context.Context, request ephemeral.ValidateConfigRequest, response *ephemeral.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } From 987f5acc80e0828b6d7d986ddcd77212a969aae1 Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 31 Oct 2024 18:47:49 +0000 Subject: [PATCH 5/5] Update changelog --- .changes/0.15.0.md | 6 ++++++ .changes/unreleased/FEATURES-20241030-164618.yaml | 6 ------ CHANGELOG.md | 6 ++++++ 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changes/0.15.0.md delete mode 100644 .changes/unreleased/FEATURES-20241030-164618.yaml diff --git a/.changes/0.15.0.md b/.changes/0.15.0.md new file mode 100644 index 00000000..dbb9e23d --- /dev/null +++ b/.changes/0.15.0.md @@ -0,0 +1,6 @@ +## 0.15.0 (October 31, 2024) + +FEATURES: + +* ephemeralvalidator: Introduce new package with declarative validators for ephemeral resource configurations ([#242](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/242)) + diff --git a/.changes/unreleased/FEATURES-20241030-164618.yaml b/.changes/unreleased/FEATURES-20241030-164618.yaml deleted file mode 100644 index 957c73cc..00000000 --- a/.changes/unreleased/FEATURES-20241030-164618.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: FEATURES -body: 'ephemeralvalidator: Introduce new package with declarative validators for ephemeral - resource configurations' -time: 2024-10-30T16:46:18.935223-04:00 -custom: - Issue: "242" diff --git a/CHANGELOG.md b/CHANGELOG.md index 82768140..11c6079f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.15.0 (October 31, 2024) + +FEATURES: + +* ephemeralvalidator: Introduce new package with declarative validators for ephemeral resource configurations ([#242](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/242)) + ## 0.14.0 (October 17, 2024) NOTES: