From c832d5d0a43d324f804603997aaa177074a64eb4 Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:30:06 -0500 Subject: [PATCH 1/7] Result of tsccr-helper -log-level=info gha update -latest .github/ (#244) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-changie.yml | 2 +- .github/workflows/ci-github-actions.yml | 4 ++-- .github/workflows/ci-go.yml | 8 ++++---- .github/workflows/ci-goreleaser.yml | 4 ++-- .github/workflows/compliance.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-changie.yml b/.github/workflows/ci-changie.yml index 33ba8ee8..3031aad8 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Ensure terraform-devex-repos is updated on version changes. - uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 with: diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 47f1bbb6..9d7e22dc 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -13,8 +13,8 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 813d4dbb..7e2da688 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,8 +16,8 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: 'go.mod' - run: go mod download @@ -30,8 +30,8 @@ jobs: matrix: go-version: [ '1.23', '1.22' ] steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version: ${{ matrix.go-version }} - run: go mod download diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index 9e5193bc..edd27522 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,8 +14,8 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: 'go.mod' - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml index 9511923d..6a82766b 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - 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 4b3613dc..7520b64c 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Avoid persisting GITHUB_TOKEN credentials as they take priority over our service account PAT for `git push` operations @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # Default input is the SHA that initially triggered the workflow. As we created a new commit in the previous job, @@ -79,12 +79,12 @@ jobs: contents: write # Needed for goreleaser to create GitHub release issues: write # Needed for goreleaser to close associated milestone steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ inputs.versionNumber }} fetch-depth: 0 - - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: 'go.mod' From 6831a7cd6a74172dd79185b48c081e218a573b41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:13:33 -0500 Subject: [PATCH 2/7] build(deps): bump github.com/golang-jwt/jwt/v4 in /tools (#247) Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.0 to 4.5.1. - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md) - [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.0...v4.5.1) --- updated-dependencies: - dependency-name: github.com/golang-jwt/jwt/v4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/go.mod | 2 +- tools/go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index 9938d4ca..738e2a3d 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -18,7 +18,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v45 v45.2.0 // indirect github.com/google/go-github/v53 v53.0.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 0d402390..cfc168fa 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -97,8 +97,9 @@ github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= +github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= From e16013a7d79f25a80fee3ea2b57f2c06a4bc6e1b Mon Sep 17 00:00:00 2001 From: "hashicorp-tsccr[bot]" <129506189+hashicorp-tsccr[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:50:58 -0500 Subject: [PATCH 3/7] Result of tsccr-helper -log-level=info gha update -latest .github/ (#248) Co-authored-by: hashicorp-tsccr[bot] --- .github/workflows/ci-goreleaser.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index edd27522..2c0ae1af 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -18,6 +18,6 @@ jobs: - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7520b64c..4899981c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: cd .changes sed -e "1{/# /d;}" -e "2{/^$/d;}" ${{ needs.changelog-version.outputs.version }}.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 7d19faa657bf4d30349d1f2f092d6f65572fccbf Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Thu, 12 Dec 2024 14:25:30 -0500 Subject: [PATCH 4/7] Add `NoNullValues` validators (#246) * listvalidator: add `NoNullValues` validator This validator will iterate over elements in the list, returning an error diagnostic if any null elements are detected. * setvalidator: add `NoNullValues` validator This validator will iterate over elements in the set, returning an error diagnostic if any null elements are detected. * mapvalidator: add `NoNullValues` validator This validator will iterate over elements in the map, returning an error diagnostic if any null elements are detected. * Documentation adjustments * changelogs --------- --- .../unreleased/FEATURES-20241212-140548.yaml | 5 + .../unreleased/FEATURES-20241212-140613.yaml | 5 + .../unreleased/FEATURES-20241212-140624.yaml | 5 + listvalidator/no_null_values.go | 78 +++++++++++++ listvalidator/no_null_values_example_test.go | 42 +++++++ listvalidator/no_null_values_test.go | 109 ++++++++++++++++++ mapvalidator/no_null_values.go | 78 +++++++++++++ mapvalidator/no_null_values_example_test.go | 42 +++++++ mapvalidator/no_null_values_test.go | 109 ++++++++++++++++++ setvalidator/no_null_values.go | 78 +++++++++++++ setvalidator/no_null_values_example_test.go | 42 +++++++ setvalidator/no_null_values_test.go | 109 ++++++++++++++++++ 12 files changed, 702 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20241212-140548.yaml create mode 100644 .changes/unreleased/FEATURES-20241212-140613.yaml create mode 100644 .changes/unreleased/FEATURES-20241212-140624.yaml create mode 100644 listvalidator/no_null_values.go create mode 100644 listvalidator/no_null_values_example_test.go create mode 100644 listvalidator/no_null_values_test.go create mode 100644 mapvalidator/no_null_values.go create mode 100644 mapvalidator/no_null_values_example_test.go create mode 100644 mapvalidator/no_null_values_test.go create mode 100644 setvalidator/no_null_values.go create mode 100644 setvalidator/no_null_values_example_test.go create mode 100644 setvalidator/no_null_values_test.go diff --git a/.changes/unreleased/FEATURES-20241212-140548.yaml b/.changes/unreleased/FEATURES-20241212-140548.yaml new file mode 100644 index 00000000..5a41a0d8 --- /dev/null +++ b/.changes/unreleased/FEATURES-20241212-140548.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'listvalidator: Added `NoNullValues` validator' +time: 2024-12-12T14:05:48.064529-05:00 +custom: + Issue: "245" diff --git a/.changes/unreleased/FEATURES-20241212-140613.yaml b/.changes/unreleased/FEATURES-20241212-140613.yaml new file mode 100644 index 00000000..91a69a9a --- /dev/null +++ b/.changes/unreleased/FEATURES-20241212-140613.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'mapvalidator: Added `NoNullValues` validator' +time: 2024-12-12T14:06:13.229216-05:00 +custom: + Issue: "245" diff --git a/.changes/unreleased/FEATURES-20241212-140624.yaml b/.changes/unreleased/FEATURES-20241212-140624.yaml new file mode 100644 index 00000000..92fb1f62 --- /dev/null +++ b/.changes/unreleased/FEATURES-20241212-140624.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'setvalidator: Added `NoNullValues` validator' +time: 2024-12-12T14:06:24.967598-05:00 +custom: + Issue: "245" diff --git a/listvalidator/no_null_values.go b/listvalidator/no_null_values.go new file mode 100644 index 00000000..a66e8b0b --- /dev/null +++ b/listvalidator/no_null_values.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.List = noNullValuesValidator{} +var _ function.ListParameterValidator = noNullValuesValidator{} + +type noNullValuesValidator struct{} + +func (v noNullValuesValidator) Description(_ context.Context) string { + return "All values in the list must be configured" +} + +func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v noNullValuesValidator) ValidateList(_ context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elements := req.ConfigValue.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Diagnostics.AddAttributeError( + req.Path, + "Null List Value", + "This attribute contains a null value.", + ) + } + } +} + +func (v noNullValuesValidator) ValidateParameterList(ctx context.Context, req function.ListParameterValidatorRequest, resp *function.ListParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elements := req.Value.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Error = function.ConcatFuncErrors( + resp.Error, + function.NewArgumentFuncError( + req.ArgumentPosition, + "Null List Value: This attribute contains a null value.", + ), + ) + } + } +} + +// NoNullValues returns a validator which ensures that any configured list +// only contains non-null values. +func NoNullValues() noNullValuesValidator { + return noNullValuesValidator{} +} diff --git a/listvalidator/no_null_values_example_test.go b/listvalidator/no_null_values_example_test.go new file mode 100644 index 00000000..c141daec --- /dev/null +++ b/listvalidator/no_null_values_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ExampleNoNullValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.List{ + // Validate this list must contain no null values. + listvalidator.NoNullValues(), + }, + }, + }, + } +} + +func ExampleNoNullValues_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.ListParameter{ + Name: "example_param", + Validators: []function.ListParameterValidator{ + // Validate this list must contain no null values. + listvalidator.NoNullValues(), + }, + }, + }, + } +} diff --git a/listvalidator/no_null_values_test.go b/listvalidator/no_null_values_test.go new file mode 100644 index 00000000..f2ae0280 --- /dev/null +++ b/listvalidator/no_null_values_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoNullValuesValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.List + expectError bool + } + tests := map[string]testCase{ + "List unknown": { + val: types.ListUnknown( + types.StringType, + ), + expectError: false, + }, + "List null": { + val: types.ListNull( + types.StringType, + ), + expectError: false, + }, + "No null values": { + val: types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringValue("second"), + }, + ), + expectError: false, + }, + "Unknown value": { + val: types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringUnknown(), + }, + ), + expectError: false, + }, + "Null value": { + val: types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringNull(), + }, + ), + expectError: true, + }, + } + + for name, test := range tests { + t.Run(fmt.Sprintf("ValidateList - %s", name), func(t *testing.T) { + t.Parallel() + request := validator.ListRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.ListResponse{} + listvalidator.NoNullValues().ValidateList(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterList - %s", name), func(t *testing.T) { + t.Parallel() + request := function.ListParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.ListParameterValidatorResponse{} + listvalidator.NoNullValues().ValidateParameterList(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) + } +} diff --git a/mapvalidator/no_null_values.go b/mapvalidator/no_null_values.go new file mode 100644 index 00000000..0c9eb69d --- /dev/null +++ b/mapvalidator/no_null_values.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package mapvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Map = noNullValuesValidator{} +var _ function.MapParameterValidator = noNullValuesValidator{} + +type noNullValuesValidator struct{} + +func (v noNullValuesValidator) Description(_ context.Context) string { + return "All values in the map must be configured" +} + +func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v noNullValuesValidator) ValidateMap(_ context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elements := req.ConfigValue.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Diagnostics.AddAttributeError( + req.Path, + "Null Map Value", + "This attribute contains a null value.", + ) + } + } +} + +func (v noNullValuesValidator) ValidateParameterMap(ctx context.Context, req function.MapParameterValidatorRequest, resp *function.MapParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elements := req.Value.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Error = function.ConcatFuncErrors( + resp.Error, + function.NewArgumentFuncError( + req.ArgumentPosition, + "Null Map Value: This attribute contains a null value.", + ), + ) + } + } +} + +// NoNullValues returns a validator which ensures that any configured map +// only contains non-null values. +func NoNullValues() noNullValuesValidator { + return noNullValuesValidator{} +} diff --git a/mapvalidator/no_null_values_example_test.go b/mapvalidator/no_null_values_example_test.go new file mode 100644 index 00000000..3a1604d4 --- /dev/null +++ b/mapvalidator/no_null_values_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package mapvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ExampleNoNullValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.MapAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Map{ + // Validate this map must contain no null values. + mapvalidator.NoNullValues(), + }, + }, + }, + } +} + +func ExampleNoNullValues_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.MapParameter{ + Name: "example_param", + Validators: []function.MapParameterValidator{ + // Validate this map must contain no null values. + mapvalidator.NoNullValues(), + }, + }, + }, + } +} diff --git a/mapvalidator/no_null_values_test.go b/mapvalidator/no_null_values_test.go new file mode 100644 index 00000000..18e05e9d --- /dev/null +++ b/mapvalidator/no_null_values_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package mapvalidator_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoNullValuesValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.Map + expectError bool + } + tests := map[string]testCase{ + "Map unknown": { + val: types.MapUnknown( + types.StringType, + ), + expectError: false, + }, + "Map null": { + val: types.MapNull( + types.StringType, + ), + expectError: false, + }, + "No null values": { + val: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "key1": types.StringValue("first"), + "key2": types.StringValue("second"), + }, + ), + expectError: false, + }, + "Unknown value": { + val: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "key1": types.StringValue("first"), + "key2": types.StringUnknown(), + }, + ), + expectError: false, + }, + "Null value": { + val: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "key1": types.StringValue("first"), + "key2": types.StringNull(), + }, + ), + expectError: true, + }, + } + + for name, test := range tests { + t.Run(fmt.Sprintf("ValidateMap - %s", name), func(t *testing.T) { + t.Parallel() + request := validator.MapRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.MapResponse{} + mapvalidator.NoNullValues().ValidateMap(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterMap - %s", name), func(t *testing.T) { + t.Parallel() + request := function.MapParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.MapParameterValidatorResponse{} + mapvalidator.NoNullValues().ValidateParameterMap(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) + } +} diff --git a/setvalidator/no_null_values.go b/setvalidator/no_null_values.go new file mode 100644 index 00000000..a4ae414c --- /dev/null +++ b/setvalidator/no_null_values.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package setvalidator + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.Set = noNullValuesValidator{} +var _ function.SetParameterValidator = noNullValuesValidator{} + +type noNullValuesValidator struct{} + +func (v noNullValuesValidator) Description(_ context.Context) string { + return "All values in the set must be configured" +} + +func (v noNullValuesValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v noNullValuesValidator) ValidateSet(_ context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + elements := req.ConfigValue.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Diagnostics.AddAttributeError( + req.Path, + "Null Set Value", + "This attribute contains a null value.", + ) + } + } +} + +func (v noNullValuesValidator) ValidateParameterSet(ctx context.Context, req function.SetParameterValidatorRequest, resp *function.SetParameterValidatorResponse) { + if req.Value.IsNull() || req.Value.IsUnknown() { + return + } + + elements := req.Value.Elements() + + for _, e := range elements { + // Only evaluate known values for null + if e.IsUnknown() { + continue + } + + if e.IsNull() { + resp.Error = function.ConcatFuncErrors( + resp.Error, + function.NewArgumentFuncError( + req.ArgumentPosition, + "Null Set Value: This attribute contains a null value.", + ), + ) + } + } +} + +// NoNullValues returns a validator which ensures that any configured set +// only contains non-null values. +func NoNullValues() noNullValuesValidator { + return noNullValuesValidator{} +} diff --git a/setvalidator/no_null_values_example_test.go b/setvalidator/no_null_values_example_test.go new file mode 100644 index 00000000..2e1de727 --- /dev/null +++ b/setvalidator/no_null_values_example_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package setvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ExampleNoNullValues() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + // Validate this set must contain no null values. + setvalidator.NoNullValues(), + }, + }, + }, + } +} + +func ExampleNoNullValues_function() { + _ = function.Definition{ + Parameters: []function.Parameter{ + function.SetParameter{ + Name: "example_param", + Validators: []function.SetParameterValidator{ + // Validate this set must contain no null values. + setvalidator.NoNullValues(), + }, + }, + }, + } +} diff --git a/setvalidator/no_null_values_test.go b/setvalidator/no_null_values_test.go new file mode 100644 index 00000000..7ae47fe3 --- /dev/null +++ b/setvalidator/no_null_values_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package setvalidator_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNoNullValuesValidator(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.Set + expectError bool + } + tests := map[string]testCase{ + "Set unknown": { + val: types.SetUnknown( + types.StringType, + ), + expectError: false, + }, + "Set null": { + val: types.SetNull( + types.StringType, + ), + expectError: false, + }, + "No null values": { + val: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringValue("second"), + }, + ), + expectError: false, + }, + "Unknown value": { + val: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringUnknown(), + }, + ), + expectError: false, + }, + "Null value": { + val: types.SetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("first"), + types.StringNull(), + }, + ), + expectError: true, + }, + } + + for name, test := range tests { + t.Run(fmt.Sprintf("ValidateSet - %s", name), func(t *testing.T) { + t.Parallel() + request := validator.SetRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.SetResponse{} + setvalidator.NoNullValues().ValidateSet(context.TODO(), request, &response) + + if !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + + t.Run(fmt.Sprintf("ValidateParameterSet - %s", name), func(t *testing.T) { + t.Parallel() + request := function.SetParameterValidatorRequest{ + ArgumentPosition: 0, + Value: test.val, + } + response := function.SetParameterValidatorResponse{} + setvalidator.NoNullValues().ValidateParameterSet(context.TODO(), request, &response) + + if response.Error == nil && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Error != nil && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Error) + } + }) + } +} From 16687f3cdd1b17924a7fb779f9335128845996c8 Mon Sep 17 00:00:00 2001 From: magodo Date: Fri, 13 Dec 2024 06:47:42 +1100 Subject: [PATCH 5/7] New package `dynamicvalidator` (#249) * Support dynamicvalidator * Pass tests * docs updates + changelog --- .../unreleased/FEATURES-20241212-144210.yaml | 5 ++ dynamicvalidator/all.go | 57 ++++++++++++++++ dynamicvalidator/all_example_test.go | 27 ++++++++ dynamicvalidator/also_requires.go | 26 +++++++ .../also_requires_example_test.go | 31 +++++++++ dynamicvalidator/any.go | 65 ++++++++++++++++++ dynamicvalidator/any_example_test.go | 26 +++++++ dynamicvalidator/any_with_all_warnings.go | 67 +++++++++++++++++++ .../any_with_all_warnings_example_test.go | 26 +++++++ dynamicvalidator/at_least_one_of.go | 27 ++++++++ .../at_least_one_of_example_test.go | 31 +++++++++ dynamicvalidator/conflicts_with.go | 27 ++++++++ .../conflicts_with_example_test.go | 31 +++++++++ dynamicvalidator/doc.go | 5 ++ dynamicvalidator/exactly_one_of.go | 28 ++++++++ .../exactly_one_of_example_test.go | 31 +++++++++ internal/schemavalidator/also_requires.go | 15 +++++ internal/schemavalidator/at_least_one_of.go | 15 +++++ internal/schemavalidator/conflicts_with.go | 15 +++++ internal/schemavalidator/exactly_one_of.go | 15 +++++ 20 files changed, 570 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20241212-144210.yaml create mode 100644 dynamicvalidator/all.go create mode 100644 dynamicvalidator/all_example_test.go create mode 100644 dynamicvalidator/also_requires.go create mode 100644 dynamicvalidator/also_requires_example_test.go create mode 100644 dynamicvalidator/any.go create mode 100644 dynamicvalidator/any_example_test.go create mode 100644 dynamicvalidator/any_with_all_warnings.go create mode 100644 dynamicvalidator/any_with_all_warnings_example_test.go create mode 100644 dynamicvalidator/at_least_one_of.go create mode 100644 dynamicvalidator/at_least_one_of_example_test.go create mode 100644 dynamicvalidator/conflicts_with.go create mode 100644 dynamicvalidator/conflicts_with_example_test.go create mode 100644 dynamicvalidator/doc.go create mode 100644 dynamicvalidator/exactly_one_of.go create mode 100644 dynamicvalidator/exactly_one_of_example_test.go diff --git a/.changes/unreleased/FEATURES-20241212-144210.yaml b/.changes/unreleased/FEATURES-20241212-144210.yaml new file mode 100644 index 00000000..ff1a9adf --- /dev/null +++ b/.changes/unreleased/FEATURES-20241212-144210.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'dynamicvalidator: New package which contains `types.Dynamic` specific validators' +time: 2024-12-12T14:42:10.189818-05:00 +custom: + Issue: "249" diff --git a/dynamicvalidator/all.go b/dynamicvalidator/all.go new file mode 100644 index 00000000..7f29e2ae --- /dev/null +++ b/dynamicvalidator/all.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// All returns a validator which ensures that any configured attribute value +// 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 ...validator.Dynamic) validator.Dynamic { + return allValidator{ + validators: validators, + } +} + +var _ validator.Dynamic = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []validator.Dynamic +} + +// 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) +} + +// ValidateDynamic performs the validation. +func (v allValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.DynamicResponse{} + + subValidator.ValidateDynamic(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/dynamicvalidator/all_example_test.go b/dynamicvalidator/all_example_test.go new file mode 100644 index 00000000..e1de7cfb --- /dev/null +++ b/dynamicvalidator/all_example_test.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAll() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Required: true, + Validators: []validator.Dynamic{ + dynamicvalidator.Any( + dynamicvalidator.Any( /* ... */ ), + dynamicvalidator.All( /* ... */ ), + ), + }, + }, + }, + } +} diff --git a/dynamicvalidator/also_requires.go b/dynamicvalidator/also_requires.go new file mode 100644 index 00000000..15113d78 --- /dev/null +++ b/dynamicvalidator/also_requires.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AlsoRequires checks that a set of path.Expression has a non-null value, +// if the current attribute or block also has a non-null value. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.RequiredTogether], +// [providervalidator.RequiredTogether], or [resourcevalidator.RequiredTogether] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute or block +// being validated. +func AlsoRequires(expressions ...path.Expression) validator.Dynamic { + return schemavalidator.AlsoRequiresValidator{ + PathExpressions: expressions, + } +} diff --git a/dynamicvalidator/also_requires_example_test.go b/dynamicvalidator/also_requires_example_test.go new file mode 100644 index 00000000..4d4bb261 --- /dev/null +++ b/dynamicvalidator/also_requires_example_test.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAlsoRequires() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Optional: true, + Validators: []validator.Dynamic{ + // Validate this attribute must be configured with other_attr. + dynamicvalidator.AlsoRequires(path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.DynamicAttribute{ + Optional: true, + }, + }, + } +} diff --git a/dynamicvalidator/any.go b/dynamicvalidator/any.go new file mode 100644 index 00000000..42086fcf --- /dev/null +++ b/dynamicvalidator/any.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// 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 ...validator.Dynamic) validator.Dynamic { + return anyValidator{ + validators: validators, + } +} + +var _ validator.Dynamic = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []validator.Dynamic +} + +// 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) +} + +// ValidateDynamic performs the validation. +func (v anyValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + for _, subValidator := range v.validators { + validateResp := &validator.DynamicResponse{} + + subValidator.ValidateDynamic(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/dynamicvalidator/any_example_test.go b/dynamicvalidator/any_example_test.go new file mode 100644 index 00000000..b7cce6d6 --- /dev/null +++ b/dynamicvalidator/any_example_test.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAny() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Required: true, + Validators: []validator.Dynamic{ + dynamicvalidator.Any( + dynamicvalidator.Any( /* ... */ ), + ), + }, + }, + }, + } +} diff --git a/dynamicvalidator/any_with_all_warnings.go b/dynamicvalidator/any_with_all_warnings.go new file mode 100644 index 00000000..c1614798 --- /dev/null +++ b/dynamicvalidator/any_with_all_warnings.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// 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 ...validator.Dynamic) validator.Dynamic { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ validator.Dynamic = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []validator.Dynamic +} + +// 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) +} + +// ValidateDynamic performs the validation. +func (v anyWithAllWarningsValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &validator.DynamicResponse{} + + subValidator.ValidateDynamic(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/dynamicvalidator/any_with_all_warnings_example_test.go b/dynamicvalidator/any_with_all_warnings_example_test.go new file mode 100644 index 00000000..8e7ebb85 --- /dev/null +++ b/dynamicvalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAnyWithAllWarnings() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Required: true, + Validators: []validator.Dynamic{ + dynamicvalidator.AnyWithAllWarnings( + dynamicvalidator.AnyWithAllWarnings( /* ... */ ), + ), + }, + }, + }, + } +} diff --git a/dynamicvalidator/at_least_one_of.go b/dynamicvalidator/at_least_one_of.go new file mode 100644 index 00000000..7a833fd4 --- /dev/null +++ b/dynamicvalidator/at_least_one_of.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// AtLeastOneOf checks that of a set of path.Expression, +// including the attribute this validator is applied to, +// at least one has a non-null value. +// +// This implements the validation logic declaratively within the tfsdk.Schema. +// Refer to [datasourcevalidator.AtLeastOneOf], +// [providervalidator.AtLeastOneOf], or [resourcevalidator.AtLeastOneOf] +// for declaring this type of validation outside the schema definition. +// +// Any relative path.Expression will be resolved using the attribute being +// validated. +func AtLeastOneOf(expressions ...path.Expression) validator.Dynamic { + return schemavalidator.AtLeastOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/dynamicvalidator/at_least_one_of_example_test.go b/dynamicvalidator/at_least_one_of_example_test.go new file mode 100644 index 00000000..6e64bbc4 --- /dev/null +++ b/dynamicvalidator/at_least_one_of_example_test.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleAtLeastOneOf() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Optional: true, + Validators: []validator.Dynamic{ + // Validate at least this attribute or other_attr should be configured. + dynamicvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.DynamicAttribute{ + Optional: true, + }, + }, + } +} diff --git a/dynamicvalidator/conflicts_with.go b/dynamicvalidator/conflicts_with.go new file mode 100644 index 00000000..1bda81c2 --- /dev/null +++ b/dynamicvalidator/conflicts_with.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// ConflictsWith checks that a set of path.Expression, +// including the attribute the validator is applied to, +// do not have a value simultaneously. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.Conflicting], +// [providervalidator.Conflicting], or [resourcevalidator.Conflicting] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute being +// validated. +func ConflictsWith(expressions ...path.Expression) validator.Dynamic { + return schemavalidator.ConflictsWithValidator{ + PathExpressions: expressions, + } +} diff --git a/dynamicvalidator/conflicts_with_example_test.go b/dynamicvalidator/conflicts_with_example_test.go new file mode 100644 index 00000000..268f765a --- /dev/null +++ b/dynamicvalidator/conflicts_with_example_test.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleConflictsWith() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Optional: true, + Validators: []validator.Dynamic{ + // Validate this attribute must not be configured with other_attr. + dynamicvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.DynamicAttribute{ + Optional: true, + }, + }, + } +} diff --git a/dynamicvalidator/doc.go b/dynamicvalidator/doc.go new file mode 100644 index 00000000..de534e26 --- /dev/null +++ b/dynamicvalidator/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package dynamicvalidator provides validators for types.Dynamic attributes and function parameters. +package dynamicvalidator diff --git a/dynamicvalidator/exactly_one_of.go b/dynamicvalidator/exactly_one_of.go new file mode 100644 index 00000000..a501c7e8 --- /dev/null +++ b/dynamicvalidator/exactly_one_of.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// ExactlyOneOf checks that of a set of path.Expression, +// including the attribute the validator is applied to, +// one and only one attribute has a value. +// It will also cause a validation error if none are specified. +// +// This implements the validation logic declaratively within the schema. +// Refer to [datasourcevalidator.ExactlyOneOf], +// [providervalidator.ExactlyOneOf], or [resourcevalidator.ExactlyOneOf] +// for declaring this type of validation outside the schema definition. +// +// Relative path.Expression will be resolved using the attribute being +// validated. +func ExactlyOneOf(expressions ...path.Expression) validator.Dynamic { + return schemavalidator.ExactlyOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/dynamicvalidator/exactly_one_of_example_test.go b/dynamicvalidator/exactly_one_of_example_test.go new file mode 100644 index 00000000..47be7cc8 --- /dev/null +++ b/dynamicvalidator/exactly_one_of_example_test.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func ExampleExactlyOneOf() { + // Used within a Schema method of a DataSource, Provider, or Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Optional: true, + Validators: []validator.Dynamic{ + // Validate only this attribute or other_attr is configured. + dynamicvalidator.ExactlyOneOf(path.Expressions{ + path.MatchRoot("other_attr"), + }...), + }, + }, + "other_attr": schema.DynamicAttribute{ + Optional: true, + }, + }, + } +} diff --git a/internal/schemavalidator/also_requires.go b/internal/schemavalidator/also_requires.go index 7abc8ae9..2f4662a7 100644 --- a/internal/schemavalidator/also_requires.go +++ b/internal/schemavalidator/also_requires.go @@ -29,6 +29,7 @@ var ( _ validator.Object = AlsoRequiresValidator{} _ validator.Set = AlsoRequiresValidator{} _ validator.String = AlsoRequiresValidator{} + _ validator.Dynamic = AlsoRequiresValidator{} ) // AlsoRequiresValidator is the underlying struct implementing AlsoRequires. @@ -257,3 +258,17 @@ func (av AlsoRequiresValidator) ValidateString(ctx context.Context, req validato resp.Diagnostics.Append(validateResp.Diagnostics...) } + +func (av AlsoRequiresValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + validateReq := AlsoRequiresValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &AlsoRequiresValidatorResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} diff --git a/internal/schemavalidator/at_least_one_of.go b/internal/schemavalidator/at_least_one_of.go index a31e4507..fa932ec2 100644 --- a/internal/schemavalidator/at_least_one_of.go +++ b/internal/schemavalidator/at_least_one_of.go @@ -29,6 +29,7 @@ var ( _ validator.Object = AtLeastOneOfValidator{} _ validator.Set = AtLeastOneOfValidator{} _ validator.String = AtLeastOneOfValidator{} + _ validator.Dynamic = AtLeastOneOfValidator{} ) // AtLeastOneOfValidator is the underlying struct implementing AtLeastOneOf. @@ -257,3 +258,17 @@ func (av AtLeastOneOfValidator) ValidateString(ctx context.Context, req validato resp.Diagnostics.Append(validateResp.Diagnostics...) } + +func (av AtLeastOneOfValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + validateReq := AtLeastOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &AtLeastOneOfValidatorResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} diff --git a/internal/schemavalidator/conflicts_with.go b/internal/schemavalidator/conflicts_with.go index 185d58e9..fd183893 100644 --- a/internal/schemavalidator/conflicts_with.go +++ b/internal/schemavalidator/conflicts_with.go @@ -29,6 +29,7 @@ var ( _ validator.Object = ConflictsWithValidator{} _ validator.Set = ConflictsWithValidator{} _ validator.String = ConflictsWithValidator{} + _ validator.Dynamic = ConflictsWithValidator{} ) // ConflictsWithValidator is the underlying struct implementing ConflictsWith. @@ -257,3 +258,17 @@ func (av ConflictsWithValidator) ValidateString(ctx context.Context, req validat resp.Diagnostics.Append(validateResp.Diagnostics...) } + +func (av ConflictsWithValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + validateReq := ConflictsWithValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &ConflictsWithValidatorResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} diff --git a/internal/schemavalidator/exactly_one_of.go b/internal/schemavalidator/exactly_one_of.go index 40af4383..b4714a71 100644 --- a/internal/schemavalidator/exactly_one_of.go +++ b/internal/schemavalidator/exactly_one_of.go @@ -29,6 +29,7 @@ var ( _ validator.Object = ExactlyOneOfValidator{} _ validator.Set = ExactlyOneOfValidator{} _ validator.String = ExactlyOneOfValidator{} + _ validator.Dynamic = ExactlyOneOfValidator{} ) // ExactlyOneOfValidator is the underlying struct implementing ExactlyOneOf. @@ -277,3 +278,17 @@ func (av ExactlyOneOfValidator) ValidateString(ctx context.Context, req validato resp.Diagnostics.Append(validateResp.Diagnostics...) } + +func (av ExactlyOneOfValidator) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + validateReq := ExactlyOneOfValidatorRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &ExactlyOneOfValidatorResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} From b500ce7214925c4023cb3eef0365e9191c16f0c8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 12 Dec 2024 17:36:06 -0500 Subject: [PATCH 6/7] internal/schemavalidator: Fix bug where unknown values were returning error diagnostics too early (#252) * internal/schemavalidator: Fix bug where unknown values were returning error diagnostics too early * changelog --- .../unreleased/BUG FIXES-20241212-152125.yaml | 6 ++++ internal/schemavalidator/also_requires.go | 3 +- .../schemavalidator/also_requires_test.go | 27 ++++++++++++++++++ internal/schemavalidator/at_least_one_of.go | 1 + .../schemavalidator/at_least_one_of_test.go | 28 +++++++++++++++++++ internal/schemavalidator/conflicts_with.go | 3 +- .../schemavalidator/conflicts_with_test.go | 27 ++++++++++++++++++ .../schemavalidator/exactly_one_of_test.go | 27 ++++++++++++++++++ 8 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/BUG FIXES-20241212-152125.yaml diff --git a/.changes/unreleased/BUG FIXES-20241212-152125.yaml b/.changes/unreleased/BUG FIXES-20241212-152125.yaml new file mode 100644 index 00000000..f54b9f04 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20241212-152125.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: Fixed bug with `ConflictsWith` and `AlsoRequires` validators where unknown values + would raise invalid diagnostics during `terraform validate`. +time: 2024-12-12T15:21:25.964001-05:00 +custom: + Issue: "251" diff --git a/internal/schemavalidator/also_requires.go b/internal/schemavalidator/also_requires.go index 2f4662a7..f2de2828 100644 --- a/internal/schemavalidator/also_requires.go +++ b/internal/schemavalidator/also_requires.go @@ -58,7 +58,8 @@ func (av AlsoRequiresValidator) MarkdownDescription(_ context.Context) string { func (av AlsoRequiresValidator) Validate(ctx context.Context, req AlsoRequiresValidatorRequest, res *AlsoRequiresValidatorResponse) { // If attribute configuration is null, there is nothing else to validate - if req.ConfigValue.IsNull() { + // If attribute configuration is unknown, delay the validation until it is known. + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } diff --git a/internal/schemavalidator/also_requires_test.go b/internal/schemavalidator/also_requires_test.go index 6c7da078..3ea0d40b 100644 --- a/internal/schemavalidator/also_requires_test.go +++ b/internal/schemavalidator/also_requires_test.go @@ -80,6 +80,33 @@ func TestAlsoRequiresValidatorValidate(t *testing.T) { path.MatchRoot("foo"), }, }, + "self-is-unknown": { + req: schemavalidator.AlsoRequiresValidatorRequest{ + ConfigValue: types.StringUnknown(), + Path: path.Root("bar"), + PathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": schema.Int64Attribute{}, + "bar": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, nil), + "bar": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, "error_missing-one": { req: schemavalidator.AlsoRequiresValidatorRequest{ ConfigValue: types.StringValue("bar value"), diff --git a/internal/schemavalidator/at_least_one_of.go b/internal/schemavalidator/at_least_one_of.go index fa932ec2..c68ea841 100644 --- a/internal/schemavalidator/at_least_one_of.go +++ b/internal/schemavalidator/at_least_one_of.go @@ -58,6 +58,7 @@ func (av AtLeastOneOfValidator) MarkdownDescription(_ context.Context) string { func (av AtLeastOneOfValidator) Validate(ctx context.Context, req AtLeastOneOfValidatorRequest, res *AtLeastOneOfValidatorResponse) { // If attribute configuration is not null, validator already succeeded. + // If attribute configuration is unknown, delay the validation until it is known. if !req.ConfigValue.IsNull() { return } diff --git a/internal/schemavalidator/at_least_one_of_test.go b/internal/schemavalidator/at_least_one_of_test.go index b9a88e34..ce2122cf 100644 --- a/internal/schemavalidator/at_least_one_of_test.go +++ b/internal/schemavalidator/at_least_one_of_test.go @@ -84,6 +84,34 @@ func TestAtLeastOneOfValidatorValidate(t *testing.T) { }, expected: &schemavalidator.AtLeastOneOfValidatorResponse{}, }, + "self-is-unknown": { + req: schemavalidator.AtLeastOneOfValidatorRequest{ + ConfigValue: types.StringUnknown(), + Path: path.Root("bar"), + PathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": schema.Int64Attribute{}, + "bar": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + expected: &schemavalidator.AtLeastOneOfValidatorResponse{}, + }, "error_none-set": { req: schemavalidator.AtLeastOneOfValidatorRequest{ ConfigValue: types.StringNull(), diff --git a/internal/schemavalidator/conflicts_with.go b/internal/schemavalidator/conflicts_with.go index fd183893..9cbc096c 100644 --- a/internal/schemavalidator/conflicts_with.go +++ b/internal/schemavalidator/conflicts_with.go @@ -58,7 +58,8 @@ func (av ConflictsWithValidator) MarkdownDescription(_ context.Context) string { func (av ConflictsWithValidator) Validate(ctx context.Context, req ConflictsWithValidatorRequest, res *ConflictsWithValidatorResponse) { // If attribute configuration is null, it cannot conflict with others - if req.ConfigValue.IsNull() { + // If attribute configuration is unknown, delay the validation until it is known. + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } diff --git a/internal/schemavalidator/conflicts_with_test.go b/internal/schemavalidator/conflicts_with_test.go index 92b9c37b..faa45a9c 100644 --- a/internal/schemavalidator/conflicts_with_test.go +++ b/internal/schemavalidator/conflicts_with_test.go @@ -139,6 +139,33 @@ func TestConflictsWithValidatorValidate(t *testing.T) { path.MatchRoot("foo"), }, }, + "self-is-unknown": { + req: schemavalidator.ConflictsWithValidatorRequest{ + ConfigValue: types.StringUnknown(), + Path: path.Root("bar"), + PathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": schema.Int64Attribute{}, + "bar": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, "error_allow-duplicate-input": { req: schemavalidator.ConflictsWithValidatorRequest{ ConfigValue: types.StringValue("bar value"), diff --git a/internal/schemavalidator/exactly_one_of_test.go b/internal/schemavalidator/exactly_one_of_test.go index c0c82b6d..cb6dd1b2 100644 --- a/internal/schemavalidator/exactly_one_of_test.go +++ b/internal/schemavalidator/exactly_one_of_test.go @@ -81,6 +81,33 @@ func TestExactlyOneOfValidator(t *testing.T) { path.MatchRoot("foo"), }, }, + "self-is-unknown": { + req: schemavalidator.ExactlyOneOfValidatorRequest{ + ConfigValue: types.StringUnknown(), + Path: path.Root("bar"), + PathExpression: path.MatchRoot("bar"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "foo": schema.Int64Attribute{}, + "bar": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.Number, + "bar": tftypes.String, + }, + }, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.Number, 42), + "bar": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + }, + in: path.Expressions{ + path.MatchRoot("foo"), + }, + }, "error_too-many": { req: schemavalidator.ExactlyOneOfValidatorRequest{ ConfigValue: types.StringValue("bar value"), From 89e17eeb9a88cd0c67a500189efb1a5e9da6674c Mon Sep 17 00:00:00 2001 From: hc-github-team-tf-provider-devex Date: Thu, 12 Dec 2024 22:38:48 +0000 Subject: [PATCH 7/7] Update changelog --- .changes/0.16.0.md | 13 +++++++++++++ .changes/unreleased/BUG FIXES-20241212-152125.yaml | 6 ------ .changes/unreleased/FEATURES-20241212-140548.yaml | 5 ----- .changes/unreleased/FEATURES-20241212-140613.yaml | 5 ----- .changes/unreleased/FEATURES-20241212-140624.yaml | 5 ----- .changes/unreleased/FEATURES-20241212-144210.yaml | 5 ----- CHANGELOG.md | 13 +++++++++++++ 7 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 .changes/0.16.0.md delete mode 100644 .changes/unreleased/BUG FIXES-20241212-152125.yaml delete mode 100644 .changes/unreleased/FEATURES-20241212-140548.yaml delete mode 100644 .changes/unreleased/FEATURES-20241212-140613.yaml delete mode 100644 .changes/unreleased/FEATURES-20241212-140624.yaml delete mode 100644 .changes/unreleased/FEATURES-20241212-144210.yaml diff --git a/.changes/0.16.0.md b/.changes/0.16.0.md new file mode 100644 index 00000000..414f04e7 --- /dev/null +++ b/.changes/0.16.0.md @@ -0,0 +1,13 @@ +## 0.16.0 (December 12, 2024) + +FEATURES: + +* listvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* mapvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* setvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* dynamicvalidator: New package which contains `types.Dynamic` specific validators ([#249](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/249)) + +BUG FIXES: + +* Fixed bug with `ConflictsWith` and `AlsoRequires` validators where unknown values would raise invalid diagnostics during `terraform validate`. ([#251](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/251)) + diff --git a/.changes/unreleased/BUG FIXES-20241212-152125.yaml b/.changes/unreleased/BUG FIXES-20241212-152125.yaml deleted file mode 100644 index f54b9f04..00000000 --- a/.changes/unreleased/BUG FIXES-20241212-152125.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: BUG FIXES -body: Fixed bug with `ConflictsWith` and `AlsoRequires` validators where unknown values - would raise invalid diagnostics during `terraform validate`. -time: 2024-12-12T15:21:25.964001-05:00 -custom: - Issue: "251" diff --git a/.changes/unreleased/FEATURES-20241212-140548.yaml b/.changes/unreleased/FEATURES-20241212-140548.yaml deleted file mode 100644 index 5a41a0d8..00000000 --- a/.changes/unreleased/FEATURES-20241212-140548.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'listvalidator: Added `NoNullValues` validator' -time: 2024-12-12T14:05:48.064529-05:00 -custom: - Issue: "245" diff --git a/.changes/unreleased/FEATURES-20241212-140613.yaml b/.changes/unreleased/FEATURES-20241212-140613.yaml deleted file mode 100644 index 91a69a9a..00000000 --- a/.changes/unreleased/FEATURES-20241212-140613.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'mapvalidator: Added `NoNullValues` validator' -time: 2024-12-12T14:06:13.229216-05:00 -custom: - Issue: "245" diff --git a/.changes/unreleased/FEATURES-20241212-140624.yaml b/.changes/unreleased/FEATURES-20241212-140624.yaml deleted file mode 100644 index 92fb1f62..00000000 --- a/.changes/unreleased/FEATURES-20241212-140624.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'setvalidator: Added `NoNullValues` validator' -time: 2024-12-12T14:06:24.967598-05:00 -custom: - Issue: "245" diff --git a/.changes/unreleased/FEATURES-20241212-144210.yaml b/.changes/unreleased/FEATURES-20241212-144210.yaml deleted file mode 100644 index ff1a9adf..00000000 --- a/.changes/unreleased/FEATURES-20241212-144210.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: FEATURES -body: 'dynamicvalidator: New package which contains `types.Dynamic` specific validators' -time: 2024-12-12T14:42:10.189818-05:00 -custom: - Issue: "249" diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c6079f..18146462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.16.0 (December 12, 2024) + +FEATURES: + +* listvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* mapvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* setvalidator: Added `NoNullValues` validator ([#245](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/245)) +* dynamicvalidator: New package which contains `types.Dynamic` specific validators ([#249](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/249)) + +BUG FIXES: + +* Fixed bug with `ConflictsWith` and `AlsoRequires` validators where unknown values would raise invalid diagnostics during `terraform validate`. ([#251](https://github.com/hashicorp/terraform-plugin-framework-validators/issues/251)) + ## 0.15.0 (October 31, 2024) FEATURES: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy