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: