From 176b75e02c5cd273fc6c1f773cd21cadae7a1d8a Mon Sep 17 00:00:00 2001 From: Steph Date: Mon, 21 Jul 2025 10:28:54 +0200 Subject: [PATCH 1/5] add ValidateListResourceConfig method to ConfigValidator --- internal/configvalidator/at_least_one_of.go | 5 +++++ internal/configvalidator/conflicting.go | 5 +++++ internal/configvalidator/exactly_one_of.go | 5 +++++ internal/configvalidator/required_together.go | 6 +++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/configvalidator/at_least_one_of.go b/internal/configvalidator/at_least_one_of.go index 03a32a35..8312456c 100644 --- a/internal/configvalidator/at_least_one_of.go +++ b/internal/configvalidator/at_least_one_of.go @@ -6,6 +6,7 @@ package configvalidator import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -50,6 +51,10 @@ func (v AtLeastOneOfValidator) ValidateEphemeralResource(ctx context.Context, re resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v AtLeastOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v AtLeastOneOfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, unknownPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/conflicting.go b/internal/configvalidator/conflicting.go index 38dfd5c4..f61982e7 100644 --- a/internal/configvalidator/conflicting.go +++ b/internal/configvalidator/conflicting.go @@ -6,6 +6,7 @@ package configvalidator import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -51,6 +52,10 @@ func (v ConflictingValidator) ValidateEphemeralResource(ctx context.Context, req resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v ConflictingValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v ConflictingValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/exactly_one_of.go b/internal/configvalidator/exactly_one_of.go index 14904d5a..5555da45 100644 --- a/internal/configvalidator/exactly_one_of.go +++ b/internal/configvalidator/exactly_one_of.go @@ -6,6 +6,7 @@ package configvalidator import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -51,6 +52,10 @@ func (v ExactlyOneOfValidator) ValidateEphemeralResource(ctx context.Context, re resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v ExactlyOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v ExactlyOneOfValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, unknownPaths path.Paths var diags diag.Diagnostics diff --git a/internal/configvalidator/required_together.go b/internal/configvalidator/required_together.go index e694e91d..8da61a7b 100644 --- a/internal/configvalidator/required_together.go +++ b/internal/configvalidator/required_together.go @@ -6,12 +6,12 @@ package configvalidator import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -51,6 +51,10 @@ func (v RequiredTogetherValidator) ValidateEphemeralResource(ctx context.Context resp.Diagnostics = v.Validate(ctx, req.Config) } +func (v RequiredTogetherValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + resp.Diagnostics = v.Validate(ctx, req.Config) +} + func (v RequiredTogetherValidator) Validate(ctx context.Context, config tfsdk.Config) diag.Diagnostics { var configuredPaths, foundPaths, unknownPaths path.Paths var diags diag.Diagnostics From 3c750ab0c1d9e865a198eea9e0ad9f35b7560c83 Mon Sep 17 00:00:00 2001 From: Steph Date: Mon, 21 Jul 2025 10:30:30 +0200 Subject: [PATCH 2/5] add ValidateListResourceConfig to WarningValidator --- internal/testvalidator/warning.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index f988ed99..78d5d804 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.go @@ -5,6 +5,7 @@ package testvalidator import ( "context" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -69,6 +70,14 @@ func WarningList(summary string, detail string) validator.List { } } +// WarningListResourceConfig returns a validator which returns a warning diagnostic. +func WarningListResourceConfig(summary string, detail string) list.ConfigValidator { + return WarningValidator{ + Summary: summary, + Detail: detail, + } +} + // WarningMap returns a validator which returns a warning diagnostic. func WarningMap(summary string, detail string) validator.Map { return WarningValidator{ @@ -171,6 +180,10 @@ func (v WarningValidator) ValidateDataSource(ctx context.Context, request dataso response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateEphemeralResource(ctx context.Context, request ephemeral.ValidateConfigRequest, response *ephemeral.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateFloat32(ctx context.Context, request validator.Float32Request, response *validator.Float32Response) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } @@ -191,6 +204,10 @@ func (v WarningValidator) ValidateList(ctx context.Context, request validator.Li response.Diagnostics.AddWarning(v.Summary, v.Detail) } +func (v WarningValidator) ValidateListResourceConfig(ctx context.Context, request list.ValidateConfigRequest, response *list.ValidateConfigResponse) { + response.Diagnostics.AddWarning(v.Summary, v.Detail) +} + func (v WarningValidator) ValidateMap(ctx context.Context, request validator.MapRequest, response *validator.MapResponse) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } @@ -211,10 +228,6 @@ func (v WarningValidator) ValidateResource(ctx context.Context, request resource response.Diagnostics.AddWarning(v.Summary, v.Detail) } -func (v WarningValidator) ValidateEphemeralResource(ctx context.Context, request ephemeral.ValidateConfigRequest, response *ephemeral.ValidateConfigResponse) { - response.Diagnostics.AddWarning(v.Summary, v.Detail) -} - func (v WarningValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) { response.Diagnostics.AddWarning(v.Summary, v.Detail) } From aedbdc56fb0afffd2a26203d63d28c7194a93604 Mon Sep 17 00:00:00 2001 From: Steph Date: Mon, 21 Jul 2025 10:30:50 +0200 Subject: [PATCH 3/5] add validators for list resource configs --- listresourcevalidator/all.go | 57 +++++ listresourcevalidator/all_example_test.go | 20 ++ listresourcevalidator/all_test.go | 176 ++++++++++++++ listresourcevalidator/any.go | 64 ++++++ listresourcevalidator/any_example_test.go | 16 ++ listresourcevalidator/any_test.go | 153 +++++++++++++ .../any_with_all_warnings.go | 67 ++++++ .../any_with_all_warnings_example_test.go | 16 ++ .../any_with_all_warnings_test.go | 214 ++++++++++++++++++ listresourcevalidator/at_least_one_of.go | 18 ++ .../at_least_one_of_example_test.go | 22 ++ listresourcevalidator/at_least_one_of_test.go | 122 ++++++++++ listresourcevalidator/conflicting.go | 18 ++ .../conflicting_example_test.go | 22 ++ listresourcevalidator/conflicting_test.go | 123 ++++++++++ listresourcevalidator/doc.go | 13 ++ listresourcevalidator/exactly_one_of.go | 18 ++ .../exactly_one_of_example_test.go | 22 ++ listresourcevalidator/exactly_one_of_test.go | 123 ++++++++++ listresourcevalidator/required_together.go | 18 ++ .../required_together_example_test.go | 22 ++ .../required_together_test.go | 123 ++++++++++ 22 files changed, 1447 insertions(+) create mode 100644 listresourcevalidator/all.go create mode 100644 listresourcevalidator/all_example_test.go create mode 100644 listresourcevalidator/all_test.go create mode 100644 listresourcevalidator/any.go create mode 100644 listresourcevalidator/any_example_test.go create mode 100644 listresourcevalidator/any_test.go create mode 100644 listresourcevalidator/any_with_all_warnings.go create mode 100644 listresourcevalidator/any_with_all_warnings_example_test.go create mode 100644 listresourcevalidator/any_with_all_warnings_test.go create mode 100644 listresourcevalidator/at_least_one_of.go create mode 100644 listresourcevalidator/at_least_one_of_example_test.go create mode 100644 listresourcevalidator/at_least_one_of_test.go create mode 100644 listresourcevalidator/conflicting.go create mode 100644 listresourcevalidator/conflicting_example_test.go create mode 100644 listresourcevalidator/conflicting_test.go create mode 100644 listresourcevalidator/doc.go create mode 100644 listresourcevalidator/exactly_one_of.go create mode 100644 listresourcevalidator/exactly_one_of_example_test.go create mode 100644 listresourcevalidator/exactly_one_of_test.go create mode 100644 listresourcevalidator/required_together.go create mode 100644 listresourcevalidator/required_together_example_test.go create mode 100644 listresourcevalidator/required_together_test.go diff --git a/listresourcevalidator/all.go b/listresourcevalidator/all.go new file mode 100644 index 00000000..ae9136c2 --- /dev/null +++ b/listresourcevalidator/all.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +// All returns a validator which ensures that any configured attribute value +// validates against all the given validators. +// +// Use of All is only necessary when used in conjunction with Any or AnyWithAllWarnings +// as the Validators field automatically applies a logical AND. +func All(validators ...list.ConfigValidator) list.ConfigValidator { + return allValidator{ + validators: validators, + } +} + +var _ list.ConfigValidator = allValidator{} + +// allValidator implements the validator. +type allValidator struct { + validators []list.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v allValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy all of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v allValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateListResourceConfig performs the validation. +func (v allValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &list.ValidateConfigResponse{} + + subValidator.ValidateListResourceConfig(ctx, req, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/listresourcevalidator/all_example_test.go b/listresourcevalidator/all_example_test.go new file mode 100644 index 00000000..6e2f9d4f --- /dev/null +++ b/listresourcevalidator/all_example_test.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" +) + +func ExampleAll() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + // The configuration must satisfy either All validator. + listresourcevalidator.Any( + listresourcevalidator.All( /* ... */ ), + listresourcevalidator.All( /* ... */ ), + ), + } +} diff --git a/listresourcevalidator/all_test.go b/listresourcevalidator/all_test.go new file mode 100644 index 00000000..12142f11 --- /dev/null +++ b/listresourcevalidator/all_test.go @@ -0,0 +1,176 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAllValidatorValidateListResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []list.ConfigValidator + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.All( + listresourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + listresourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.All( + listresourcevalidator.AtLeastOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + listresourcevalidator.Conflicting( + path.MatchRoot("test3"), + path.MatchRoot("test5"), + ), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + "test5": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + "test5": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + "test5": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.WithPath(path.Root("test3"), + diag.NewErrorDiagnostic( + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test3,test5]", + )), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &list.ValidateConfigResponse{} + + listresourcevalidator.Any(testCase.validators...).ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/any.go b/listresourcevalidator/any.go new file mode 100644 index 00000000..616cd230 --- /dev/null +++ b/listresourcevalidator/any.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/list" + "strings" +) + +// 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 ...list.ConfigValidator) list.ConfigValidator { + return anyValidator{ + validators: validators, + } +} + +var _ list.ConfigValidator = anyValidator{} + +// anyValidator implements the validator. +type anyValidator struct { + validators []list.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateListResourceConfig performs the validation. +func (v anyValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + for _, subValidator := range v.validators { + validateResp := &list.ValidateConfigResponse{} + + subValidator.ValidateListResourceConfig(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + resp.Diagnostics = validateResp.Diagnostics + + return + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } +} diff --git a/listresourcevalidator/any_example_test.go b/listresourcevalidator/any_example_test.go new file mode 100644 index 00000000..8b5eec75 --- /dev/null +++ b/listresourcevalidator/any_example_test.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" +) + +func ExampleAny() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + listresourcevalidator.Any( /* ... */ ), + } +} diff --git a/listresourcevalidator/any_test.go b/listresourcevalidator/any_test.go new file mode 100644 index 00000000..e850af34 --- /dev/null +++ b/listresourcevalidator/any_test.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAnyValidatorValidateListResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []list.ConfigValidator + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &list.ValidateConfigResponse{} + + listresourcevalidator.Any(testCase.validators...).ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/any_with_all_warnings.go b/listresourcevalidator/any_with_all_warnings.go new file mode 100644 index 00000000..2a4aaeae --- /dev/null +++ b/listresourcevalidator/any_with_all_warnings.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/list" +) + +// 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 ...list.ConfigValidator) list.ConfigValidator { + return anyWithAllWarningsValidator{ + validators: validators, + } +} + +var _ list.ConfigValidator = anyWithAllWarningsValidator{} + +// anyWithAllWarningsValidator implements the validator. +type anyWithAllWarningsValidator struct { + validators []list.ConfigValidator +} + +// Description describes the validation in plain text formatting. +func (v anyWithAllWarningsValidator) Description(ctx context.Context) string { + var descriptions []string + + for _, subValidator := range v.validators { + descriptions = append(descriptions, subValidator.Description(ctx)) + } + + return fmt.Sprintf("Value must satisfy at least one of the validations: %s", strings.Join(descriptions, " + ")) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v anyWithAllWarningsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateListResourceConfig performs the validation. +func (v anyWithAllWarningsValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { + anyValid := false + + for _, subValidator := range v.validators { + validateResp := &list.ValidateConfigResponse{} + + subValidator.ValidateListResourceConfig(ctx, req, validateResp) + + if !validateResp.Diagnostics.HasError() { + anyValid = true + } + + resp.Diagnostics.Append(validateResp.Diagnostics...) + } + + if anyValid { + resp.Diagnostics = resp.Diagnostics.Warnings() + } +} diff --git a/listresourcevalidator/any_with_all_warnings_example_test.go b/listresourcevalidator/any_with_all_warnings_example_test.go new file mode 100644 index 00000000..74667486 --- /dev/null +++ b/listresourcevalidator/any_with_all_warnings_example_test.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" +) + +func ExampleAnyWithAllWarnings() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + listresourcevalidator.AnyWithAllWarnings( /* ... */ ), + } +} diff --git a/listresourcevalidator/any_with_all_warnings_test.go b/listresourcevalidator/any_with_all_warnings_test.go new file mode 100644 index 00000000..8d98adc3 --- /dev/null +++ b/listresourcevalidator/any_with_all_warnings_test.go @@ -0,0 +1,214 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAnyWithAllWarningsValidatorValidateListResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []list.ConfigValidator + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "valid": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "valid with warning": { + validators: []list.ConfigValidator{ + listresourcevalidator.All( + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + testvalidator.WarningListResourceConfig("failing warning summary", "failing warning details"), + ), + listresourcevalidator.All( + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + testvalidator.WarningListResourceConfig("passing warning summary", "passing warning details"), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, "test-value"), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("failing warning summary", "failing warning details"), + diag.NewWarningDiagnostic("passing warning summary", "passing warning details"), + }, + }, + }, + "invalid": { + validators: []list.ConfigValidator{ + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test1"), + path.MatchRoot("test2"), + ), + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("test3"), + path.MatchRoot("test4"), + ), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "test3": schema.StringAttribute{ + Optional: true, + }, + "test4": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "test3": tftypes.String, + "test4": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "test3": tftypes.NewValue(tftypes.String, nil), + "test4": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "Exactly one of these attributes must be configured: [test3,test4]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &list.ValidateConfigResponse{} + + listresourcevalidator.AnyWithAllWarnings(testCase.validators...).ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/at_least_one_of.go b/listresourcevalidator/at_least_one_of.go new file mode 100644 index 00000000..106e9675 --- /dev/null +++ b/listresourcevalidator/at_least_one_of.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// AtLeastOneOf checks that a set of path.Expression has at least one non-null +// or unknown value. +func AtLeastOneOf(expressions ...path.Expression) list.ConfigValidator { + return &configvalidator.AtLeastOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/listresourcevalidator/at_least_one_of_example_test.go b/listresourcevalidator/at_least_one_of_example_test.go new file mode 100644 index 00000000..1072073b --- /dev/null +++ b/listresourcevalidator/at_least_one_of_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleAtLeastOneOf() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + // Validate at least one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value. + listresourcevalidator.AtLeastOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/listresourcevalidator/at_least_one_of_test.go b/listresourcevalidator/at_least_one_of_test.go new file mode 100644 index 00000000..e7be84dd --- /dev/null +++ b/listresourcevalidator/at_least_one_of_test.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAtLeastOneOf(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, nil), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Attribute Configuration", + "At least one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := listresourcevalidator.AtLeastOneOf(testCase.pathExpressions...) + got := &list.ValidateConfigResponse{} + + validator.ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/conflicting.go b/listresourcevalidator/conflicting.go new file mode 100644 index 00000000..eafa2a75 --- /dev/null +++ b/listresourcevalidator/conflicting.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Conflicting checks that a set of path.Expression, are not configured +// simultaneously. +func Conflicting(expressions ...path.Expression) list.ConfigValidator { + return &configvalidator.ConflictingValidator{ + PathExpressions: expressions, + } +} diff --git a/listresourcevalidator/conflicting_example_test.go b/listresourcevalidator/conflicting_example_test.go new file mode 100644 index 00000000..899f56d3 --- /dev/null +++ b/listresourcevalidator/conflicting_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleConflicting() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + // Validate that schema defined attributes named attr1 and attr2 are not + // both configured with known, non-null values. + listresourcevalidator.Conflicting( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/listresourcevalidator/conflicting_test.go b/listresourcevalidator/conflicting_test.go new file mode 100644 index 00000000..848744f1 --- /dev/null +++ b/listresourcevalidator/conflicting_test.go @@ -0,0 +1,123 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestConflicting(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes cannot be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := listresourcevalidator.Conflicting(testCase.pathExpressions...) + got := &list.ValidateConfigResponse{} + + validator.ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/doc.go b/listresourcevalidator/doc.go new file mode 100644 index 00000000..5453d9c1 --- /dev/null +++ b/listresourcevalidator/doc.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package listresourcevalidator provides validators to express relationships +// between multiple attributes of a list config. For example, checking that +// multiple attributes are not configured at the same time. +// +// These validators are implemented outside the schema, which may be easier to +// implement in provider code generation situations or suit provider code +// preferences differently than those in the schemavalidator package. Those +// validators start on a starting attribute, where relationships can be +// expressed as absolute paths to others or relative to the starting attribute. +package listresourcevalidator diff --git a/listresourcevalidator/exactly_one_of.go b/listresourcevalidator/exactly_one_of.go new file mode 100644 index 00000000..06f02dc9 --- /dev/null +++ b/listresourcevalidator/exactly_one_of.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// ExactlyOneOf checks that a set of path.Expression does not have more than +// one known value. +func ExactlyOneOf(expressions ...path.Expression) list.ConfigValidator { + return &configvalidator.ExactlyOneOfValidator{ + PathExpressions: expressions, + } +} diff --git a/listresourcevalidator/exactly_one_of_example_test.go b/listresourcevalidator/exactly_one_of_example_test.go new file mode 100644 index 00000000..7ccd677e --- /dev/null +++ b/listresourcevalidator/exactly_one_of_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleExactlyOneOf() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + // Validate only one of the schema defined attributes named attr1 + // and attr2 has a known, non-null value. + listresourcevalidator.ExactlyOneOf( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/listresourcevalidator/exactly_one_of_test.go b/listresourcevalidator/exactly_one_of_test.go new file mode 100644 index 00000000..8ff52764 --- /dev/null +++ b/listresourcevalidator/exactly_one_of_test.go @@ -0,0 +1,123 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestExactlyOneOf(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "Exactly one of these attributes must be configured: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := listresourcevalidator.ExactlyOneOf(testCase.pathExpressions...) + got := &list.ValidateConfigResponse{} + + validator.ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/listresourcevalidator/required_together.go b/listresourcevalidator/required_together.go new file mode 100644 index 00000000..4c4fc630 --- /dev/null +++ b/listresourcevalidator/required_together.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// RequiredTogether checks that a set of path.Expression either has all known +// or all null values. +func RequiredTogether(expressions ...path.Expression) list.ConfigValidator { + return &configvalidator.RequiredTogetherValidator{ + PathExpressions: expressions, + } +} diff --git a/listresourcevalidator/required_together_example_test.go b/listresourcevalidator/required_together_example_test.go new file mode 100644 index 00000000..cded78e4 --- /dev/null +++ b/listresourcevalidator/required_together_example_test.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +func ExampleRequiredTogether() { + // Used inside a list.ListResource type ConfigValidators method + _ = []list.ConfigValidator{ + // Validate the schema defined attributes named attr1 and attr2 are either + // both null or both known values. + listresourcevalidator.RequiredTogether( + path.MatchRoot("attr1"), + path.MatchRoot("attr2"), + ), + } +} diff --git a/listresourcevalidator/required_together_test.go b/listresourcevalidator/required_together_test.go new file mode 100644 index 00000000..bfdd6a8d --- /dev/null +++ b/listresourcevalidator/required_together_test.go @@ -0,0 +1,123 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listresourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestRequiredTogether(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + pathExpressions path.Expressions + req list.ValidateConfigRequest + expected *list.ValidateConfigResponse + }{ + "no-diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{}, + }, + "diagnostics": { + pathExpressions: path.Expressions{ + path.MatchRoot("test1"), + path.MatchRoot("test2"), + }, + req: list.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test1": schema.StringAttribute{ + Optional: true, + }, + "test2": schema.StringAttribute{ + Optional: true, + }, + "other": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test1": tftypes.String, + "test2": tftypes.String, + "other": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test1": tftypes.NewValue(tftypes.String, "test-value"), + "test2": tftypes.NewValue(tftypes.String, nil), + "other": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + }, + expected: &list.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test1"), + "Invalid Attribute Combination", + "These attributes must be configured together: [test1,test2]", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + validator := listresourcevalidator.RequiredTogether(testCase.pathExpressions...) + got := &list.ValidateConfigResponse{} + + validator.ValidateListResourceConfig(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 0de0ad9410be31dc3b1fce969568025e21073a59 Mon Sep 17 00:00:00 2001 From: Steph Date: Mon, 21 Jul 2025 10:39:41 +0200 Subject: [PATCH 4/5] fix import ordering --- internal/configvalidator/at_least_one_of.go | 10 +++++----- internal/configvalidator/conflicting.go | 10 +++++----- internal/configvalidator/exactly_one_of.go | 10 +++++----- internal/configvalidator/required_together.go | 9 +++++---- internal/testvalidator/warning.go | 2 +- listresourcevalidator/any_with_all_warnings_test.go | 2 +- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/internal/configvalidator/at_least_one_of.go b/internal/configvalidator/at_least_one_of.go index 8312456c..c4b9c82c 100644 --- a/internal/configvalidator/at_least_one_of.go +++ b/internal/configvalidator/at_least_one_of.go @@ -6,12 +6,12 @@ package configvalidator import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -39,19 +39,19 @@ func (v AtLeastOneOfValidator) ValidateDataSource(ctx context.Context, req datas resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v AtLeastOneOfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { +func (v AtLeastOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v AtLeastOneOfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (v AtLeastOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v AtLeastOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { +func (v AtLeastOneOfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v AtLeastOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { +func (v AtLeastOneOfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } diff --git a/internal/configvalidator/conflicting.go b/internal/configvalidator/conflicting.go index f61982e7..bd867e96 100644 --- a/internal/configvalidator/conflicting.go +++ b/internal/configvalidator/conflicting.go @@ -6,13 +6,13 @@ package configvalidator import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -40,19 +40,19 @@ func (v ConflictingValidator) ValidateDataSource(ctx context.Context, req dataso resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ConflictingValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { +func (v ConflictingValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ConflictingValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (v ConflictingValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ConflictingValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { +func (v ConflictingValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ConflictingValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { +func (v ConflictingValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } diff --git a/internal/configvalidator/exactly_one_of.go b/internal/configvalidator/exactly_one_of.go index 5555da45..b0b2f011 100644 --- a/internal/configvalidator/exactly_one_of.go +++ b/internal/configvalidator/exactly_one_of.go @@ -6,13 +6,13 @@ package configvalidator import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -40,19 +40,19 @@ func (v ExactlyOneOfValidator) ValidateDataSource(ctx context.Context, req datas resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ExactlyOneOfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { +func (v ExactlyOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ExactlyOneOfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (v ExactlyOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ExactlyOneOfValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { +func (v ExactlyOneOfValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v ExactlyOneOfValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { +func (v ExactlyOneOfValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } diff --git a/internal/configvalidator/required_together.go b/internal/configvalidator/required_together.go index 8da61a7b..f5159d67 100644 --- a/internal/configvalidator/required_together.go +++ b/internal/configvalidator/required_together.go @@ -6,6 +6,7 @@ package configvalidator import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -39,19 +40,19 @@ func (v RequiredTogetherValidator) ValidateDataSource(ctx context.Context, req d resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v RequiredTogetherValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { +func (v RequiredTogetherValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v RequiredTogetherValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { +func (v RequiredTogetherValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v RequiredTogetherValidator) ValidateEphemeralResource(ctx context.Context, req ephemeral.ValidateConfigRequest, resp *ephemeral.ValidateConfigResponse) { +func (v RequiredTogetherValidator) ValidateProvider(ctx context.Context, req provider.ValidateConfigRequest, resp *provider.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } -func (v RequiredTogetherValidator) ValidateListResourceConfig(ctx context.Context, req list.ValidateConfigRequest, resp *list.ValidateConfigResponse) { +func (v RequiredTogetherValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { resp.Diagnostics = v.Validate(ctx, req.Config) } diff --git a/internal/testvalidator/warning.go b/internal/testvalidator/warning.go index 78d5d804..e2eb3578 100644 --- a/internal/testvalidator/warning.go +++ b/internal/testvalidator/warning.go @@ -5,10 +5,10 @@ package testvalidator import ( "context" - "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" diff --git a/listresourcevalidator/any_with_all_warnings_test.go b/listresourcevalidator/any_with_all_warnings_test.go index 8d98adc3..9757f399 100644 --- a/listresourcevalidator/any_with_all_warnings_test.go +++ b/listresourcevalidator/any_with_all_warnings_test.go @@ -5,11 +5,11 @@ package listresourcevalidator_test import ( "context" - "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework-validators/internal/testvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/listresourcevalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/list/schema" From 3ed9fb8080974b5a8f109c0cf1587780c6e1e7a8 Mon Sep 17 00:00:00 2001 From: Steph Date: Tue, 22 Jul 2025 08:32:11 +0200 Subject: [PATCH 5/5] update dependencies --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 49d5009c..fce369ff 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-framework v1.15.0 - github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-framework v1.15.1-0.20250721151353-59a937e815ac + github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 ) require ( @@ -19,5 +19,5 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/sys v0.32.0 // indirect + golang.org/x/sys v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 8fe83ea9..c4b57a02 100644 --- a/go.sum +++ b/go.sum @@ -7,10 +7,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-framework v1.15.0 h1:LQ2rsOfmDLxcn5EeIwdXFtr03FVsNktbbBci8cOKdb4= -github.com/hashicorp/terraform-plugin-framework v1.15.0/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI= -github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= -github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-framework v1.15.1-0.20250721151353-59a937e815ac h1:yJgGua9+kWvU3p/QeUShF5BV5L7YW+fEdhuWk90VdJw= +github.com/hashicorp/terraform-plugin-framework v1.15.1-0.20250721151353-59a937e815ac/go.mod h1:1aeefX7ICeY56E8o1t9V0RSPa1DKkiwpPTihj8RfVRs= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806 h1:i3kA1sT/Fk8Ex+VVKdjf9sFOPwS7w3Q73pfbnxKwdjg= +github.com/hashicorp/terraform-plugin-go v0.29.0-alpha.1.0.20250709165734-a8477a15f806/go.mod h1:hL//wLEfYo0YVt0TC/VLzia/ADQQto3HEm4/jX2gkdY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -37,8 +37,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=