diff --git a/.changes/unreleased/FEATURES-20250206-120903.yaml b/.changes/unreleased/FEATURES-20250206-120903.yaml new file mode 100644 index 00000000..41c57026 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-120903.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'boolvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:09:03.733715-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121130.yaml b/.changes/unreleased/FEATURES-20250206-121130.yaml new file mode 100644 index 00000000..8b43f1c2 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121130.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'dynamicvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:11:30.572285-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121159.yaml b/.changes/unreleased/FEATURES-20250206-121159.yaml new file mode 100644 index 00000000..9e283b24 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121159.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'float32validator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:11:59.137017-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121224.yaml b/.changes/unreleased/FEATURES-20250206-121224.yaml new file mode 100644 index 00000000..c843d96c --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121224.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'float64validator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:12:24.396457-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121243.yaml b/.changes/unreleased/FEATURES-20250206-121243.yaml new file mode 100644 index 00000000..f02e8e6b --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121243.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'int32validator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:12:43.521669-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121308.yaml b/.changes/unreleased/FEATURES-20250206-121308.yaml new file mode 100644 index 00000000..d8f1d584 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121308.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'int64validator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:13:08.483387-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121402.yaml b/.changes/unreleased/FEATURES-20250206-121402.yaml new file mode 100644 index 00000000..a9bb8677 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121402.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'listvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:14:02.302221-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121418.yaml b/.changes/unreleased/FEATURES-20250206-121418.yaml new file mode 100644 index 00000000..908ef680 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121418.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'mapvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:14:18.324953-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121440.yaml b/.changes/unreleased/FEATURES-20250206-121440.yaml new file mode 100644 index 00000000..0b132279 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121440.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'numbervalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:14:40.942642-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121500.yaml b/.changes/unreleased/FEATURES-20250206-121500.yaml new file mode 100644 index 00000000..bd4d7222 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121500.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'objectvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:15:00.176757-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121519.yaml b/.changes/unreleased/FEATURES-20250206-121519.yaml new file mode 100644 index 00000000..d519065a --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121519.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'resourcevalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:15:19.618622-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121533.yaml b/.changes/unreleased/FEATURES-20250206-121533.yaml new file mode 100644 index 00000000..81104d33 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121533.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'setvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:15:33.908556-05:00 +custom: + Issue: "263" diff --git a/.changes/unreleased/FEATURES-20250206-121553.yaml b/.changes/unreleased/FEATURES-20250206-121553.yaml new file mode 100644 index 00000000..c39aa29a --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-121553.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'stringvalidator: Added `PreferWriteOnlyAttribute` validator' +time: 2025-02-06T12:15:53.337804-05:00 +custom: + Issue: "263" diff --git a/boolvalidator/prefer_write_only_attribute.go b/boolvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..712c0a91 --- /dev/null +++ b/boolvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Bool { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/boolvalidator/prefer_write_only_attribute_example_test.go b/boolvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..05b2ac0c --- /dev/null +++ b/boolvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package boolvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.BoolAttribute{ + Optional: true, + Validators: []validator.Bool{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + boolvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.BoolAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/dynamicvalidator/prefer_write_only_attribute.go b/dynamicvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..811d0f5b --- /dev/null +++ b/dynamicvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Dynamic { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/dynamicvalidator/prefer_write_only_attribute_example_test.go b/dynamicvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..62491f62 --- /dev/null +++ b/dynamicvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package dynamicvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/dynamicvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.DynamicAttribute{ + Optional: true, + Validators: []validator.Dynamic{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + dynamicvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.DynamicAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/float32validator/prefer_write_only_attribute.go b/float32validator/prefer_write_only_attribute.go new file mode 100644 index 00000000..b5ff8670 --- /dev/null +++ b/float32validator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32validator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Float32 { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/float32validator/prefer_write_only_attribute_example_test.go b/float32validator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..50754649 --- /dev/null +++ b/float32validator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float32validator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/float32validator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.Float32Attribute{ + Optional: true, + Validators: []validator.Float32{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + float32validator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.Float32Attribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/float64validator/prefer_write_only_attribute.go b/float64validator/prefer_write_only_attribute.go new file mode 100644 index 00000000..8882cf73 --- /dev/null +++ b/float64validator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Float64 { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/float64validator/prefer_write_only_attribute_example_test.go b/float64validator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..e3c2e541 --- /dev/null +++ b/float64validator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package float64validator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.Float64Attribute{ + Optional: true, + Validators: []validator.Float64{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + float64validator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.Float64Attribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/int32validator/prefer_write_only_attribute.go b/int32validator/prefer_write_only_attribute.go new file mode 100644 index 00000000..c064220d --- /dev/null +++ b/int32validator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32validator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Int32 { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/int32validator/prefer_write_only_attribute_example_test.go b/int32validator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..3ada7a75 --- /dev/null +++ b/int32validator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int32validator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.Int32Attribute{ + Optional: true, + Validators: []validator.Int32{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + int32validator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.Int32Attribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/int64validator/prefer_write_only_attribute.go b/int64validator/prefer_write_only_attribute.go new file mode 100644 index 00000000..3161328f --- /dev/null +++ b/int64validator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Int64 { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/int64validator/prefer_write_only_attribute_example_test.go b/int64validator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..9c894abb --- /dev/null +++ b/int64validator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package int64validator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + int64validator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.Int64Attribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/internal/schemavalidator/prefer_write_only_attribute.go b/internal/schemavalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..fa58406d --- /dev/null +++ b/internal/schemavalidator/prefer_write_only_attribute.go @@ -0,0 +1,249 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schemavalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// This type of validator must satisfy all types. +var ( + _ validator.Bool = PreferWriteOnlyAttribute{} + _ validator.Float32 = PreferWriteOnlyAttribute{} + _ validator.Float64 = PreferWriteOnlyAttribute{} + _ validator.Int32 = PreferWriteOnlyAttribute{} + _ validator.Int64 = PreferWriteOnlyAttribute{} + _ validator.List = PreferWriteOnlyAttribute{} + _ validator.Map = PreferWriteOnlyAttribute{} + _ validator.Number = PreferWriteOnlyAttribute{} + _ validator.Object = PreferWriteOnlyAttribute{} + _ validator.String = PreferWriteOnlyAttribute{} +) + +// PreferWriteOnlyAttribute is the underlying struct implementing ExactlyOneOf. +type PreferWriteOnlyAttribute struct { + WriteOnlyAttribute path.Expression +} + +type PreferWriteOnlyAttributeRequest struct { + ClientCapabilities validator.ValidateSchemaClientCapabilities + Config tfsdk.Config + ConfigValue attr.Value + Path path.Path + PathExpression path.Expression +} + +type PreferWriteOnlyAttributeResponse struct { + Diagnostics diag.Diagnostics +} + +func (av PreferWriteOnlyAttribute) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av PreferWriteOnlyAttribute) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("The write-only attribute %s should be preferred over this attribute", av.WriteOnlyAttribute) +} + +func (av PreferWriteOnlyAttribute) Validate(ctx context.Context, req PreferWriteOnlyAttributeRequest, resp *PreferWriteOnlyAttributeResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + return + } + + oldAttributePaths, oldAttributeDiags := req.Config.PathMatches(ctx, req.PathExpression) + if oldAttributeDiags.HasError() { + resp.Diagnostics.Append(oldAttributeDiags...) + return + } + + _, writeOnlyAttributeDiags := req.Config.PathMatches(ctx, av.WriteOnlyAttribute) + if writeOnlyAttributeDiags.HasError() { + resp.Diagnostics.Append(writeOnlyAttributeDiags...) + return + } + + for _, mp := range oldAttributePaths { + // Get the value + var matchedValue attr.Value + diags := req.Config.GetAttribute(ctx, mp, &matchedValue) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + continue + } + + resp.Diagnostics.AddAttributeWarning(mp, + "Available Write-Only Attribute Alternative", + fmt.Sprintf("This attribute has a WriteOnly version %s available. "+ + "Use the WriteOnly version of the attribute when possible.", av.WriteOnlyAttribute.String())) + } +} + +func (av PreferWriteOnlyAttribute) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateDynamic(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateFloat32(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateFloat64(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateInt32(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateInt64(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateMap(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateNumber(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateObject(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} + +func (av PreferWriteOnlyAttribute) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + validateReq := PreferWriteOnlyAttributeRequest{ + Config: req.Config, + ConfigValue: req.ConfigValue, + Path: req.Path, + PathExpression: req.PathExpression, + } + validateResp := &PreferWriteOnlyAttributeResponse{} + + av.Validate(ctx, validateReq, validateResp) + + resp.Diagnostics.Append(validateResp.Diagnostics...) +} diff --git a/internal/schemavalidator/prefer_write_only_attribute_test.go b/internal/schemavalidator/prefer_write_only_attribute_test.go new file mode 100644 index 00000000..6584fadc --- /dev/null +++ b/internal/schemavalidator/prefer_write_only_attribute_test.go @@ -0,0 +1,189 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schemavalidator_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +func TestPreferWriteOnlyAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + req schemavalidator.PreferWriteOnlyAttributeRequest + in path.Expression + expWarnings int + expErrors int + } + + testCases := map[string]testCase{ + "base": { + req: schemavalidator.PreferWriteOnlyAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + ConfigValue: types.StringValue("oldAttribute value"), + Path: path.Root("oldAttribute"), + PathExpression: path.MatchRoot("oldAttribute"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeOnlyAttribute": schema.StringAttribute{WriteOnly: true}, + "oldAttribute": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "writeOnlyAttribute": tftypes.String, + "oldAttribute": tftypes.String, + }, + }, map[string]tftypes.Value{ + "writeOnlyAttribute": tftypes.NewValue(tftypes.String, nil), + "oldAttribute": tftypes.NewValue(tftypes.String, "oldAttribute value"), + }), + }, + }, + in: path.MatchRoot("writeOnlyAttribute"), + expWarnings: 1, + }, + "no-write-only-capability": { + req: schemavalidator.PreferWriteOnlyAttributeRequest{ + ConfigValue: types.StringValue("oldAttribute value"), + Path: path.Root("oldAttribute"), + PathExpression: path.MatchRoot("oldAttribute"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeOnlyAttribute": schema.StringAttribute{WriteOnly: true}, + "oldAttribute": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "writeOnlyAttribute": tftypes.String, + "oldAttribute": tftypes.String, + }, + }, map[string]tftypes.Value{ + "writeOnlyAttribute": tftypes.NewValue(tftypes.String, nil), + "oldAttribute": tftypes.NewValue(tftypes.String, "oldAttribute value"), + }), + }, + }, + in: path.MatchRoot("writeOnlyAttribute"), + }, + "old-attribute-is-null": { + req: schemavalidator.PreferWriteOnlyAttributeRequest{ + ConfigValue: types.StringNull(), + Path: path.Root("oldAttribute"), + PathExpression: path.MatchRoot("oldAttribute"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeOnlyAttribute": schema.StringAttribute{WriteOnly: true}, + "oldAttribute": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "writeOnlyAttribute": tftypes.String, + "oldAttribute": tftypes.String, + }, + }, map[string]tftypes.Value{ + "writeOnlyAttribute": tftypes.NewValue(tftypes.String, nil), + "oldAttribute": tftypes.NewValue(tftypes.String, nil), + }), + }, + }, + in: path.MatchRoot("writeOnlyAttribute"), + }, + "old-attribute-is-unknown": { + req: schemavalidator.PreferWriteOnlyAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + ConfigValue: types.StringUnknown(), + Path: path.Root("oldAttribute"), + PathExpression: path.MatchRoot("oldAttribute"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeOnlyAttribute": schema.StringAttribute{WriteOnly: true}, + "oldAttribute": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "writeOnlyAttribute": tftypes.String, + "oldAttribute": tftypes.String, + }, + }, map[string]tftypes.Value{ + "writeOnlyAttribute": tftypes.NewValue(tftypes.String, nil), + "oldAttribute": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + }), + }, + }, + in: path.MatchRoot("writeOnlyAttribute"), + }, + "matches-no-attribute-in-schema": { + req: schemavalidator.PreferWriteOnlyAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + ConfigValue: types.StringValue("oldAttribute value"), + Path: path.Root("oldAttribute"), + PathExpression: path.MatchRoot("oldAttribute"), + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeOnlyAttribute": schema.StringAttribute{WriteOnly: true}, + "oldAttribute": schema.StringAttribute{}, + }, + }, + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "writeOnlyAttribute": tftypes.String, + "oldAttribute": tftypes.String, + }, + }, map[string]tftypes.Value{ + "writeOnlyAttribute": tftypes.NewValue(tftypes.String, nil), + "oldAttribute": tftypes.NewValue(tftypes.String, "oldAttribute value"), + }), + }, + }, + in: path.MatchRoot("writeOnlyAttribute2"), + expErrors: 1, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + res := &schemavalidator.PreferWriteOnlyAttributeResponse{} + + schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: test.in, + }.Validate(context.TODO(), test.req, res) + + if test.expWarnings != res.Diagnostics.WarningsCount() { + t.Fatalf("expected no warining(s), got %d: %v", res.Diagnostics.WarningsCount(), res.Diagnostics) + } + + if test.expWarnings > 0 && test.expWarnings != res.Diagnostics.WarningsCount() { + t.Fatalf("expected %d warning(s), got %d: %v", test.expWarnings, res.Diagnostics.WarningsCount(), res.Diagnostics) + } + + if test.expErrors == 0 && res.Diagnostics.HasError() { + t.Fatalf("expected no error(s), got %d: %v", res.Diagnostics.WarningsCount(), res.Diagnostics) + } + + if test.expErrors > 0 && test.expErrors != res.Diagnostics.ErrorsCount() { + t.Fatalf("expected %d error(s), got %d: %v", test.expErrors, res.Diagnostics.Errors(), res.Diagnostics) + } + }) + } +} diff --git a/listvalidator/prefer_write_only_attribute.go b/listvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..50aba3b4 --- /dev/null +++ b/listvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.List { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/listvalidator/prefer_write_only_attribute_example_test.go b/listvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..dc81b3f6 --- /dev/null +++ b/listvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package listvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + listvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.ListAttribute{ + ElementType: types.StringType, + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/mapvalidator/prefer_write_only_attribute.go b/mapvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..8343ef41 --- /dev/null +++ b/mapvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package mapvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Map { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/mapvalidator/prefer_write_only_attribute_example_test.go b/mapvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..a99a4155 --- /dev/null +++ b/mapvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package mapvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Map{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + mapvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.MapAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/numbervalidator/prefer_write_only_attribute.go b/numbervalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..61285843 --- /dev/null +++ b/numbervalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package numbervalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Number { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/numbervalidator/prefer_write_only_attribute_example_test.go b/numbervalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..ea1cb436 --- /dev/null +++ b/numbervalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package numbervalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/numbervalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.NumberAttribute{ + Optional: true, + Validators: []validator.Number{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + numbervalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.NumberAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/objectvalidator/prefer_write_only_attribute.go b/objectvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..71a8ac41 --- /dev/null +++ b/objectvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package objectvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.Object { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/objectvalidator/prefer_write_only_attribute_example_test.go b/objectvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..4546771a --- /dev/null +++ b/objectvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package objectvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.ObjectAttribute{ + Optional: true, + Validators: []validator.Object{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + objectvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.ObjectAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +} diff --git a/resourcevalidator/prefer_write_only_attribute.go b/resourcevalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..a2dcbec4 --- /dev/null +++ b/resourcevalidator/prefer_write_only_attribute.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resourcevalidator + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the old attribute value is not null. +func PreferWriteOnlyAttribute(oldAttribute path.Expression, writeOnlyAttribute path.Expression) resource.ConfigValidator { + return preferWriteOnlyAttributeValidator{ + oldAttribute: oldAttribute, + writeOnlyAttribute: writeOnlyAttribute, + } +} + +var _ resource.ConfigValidator = preferWriteOnlyAttributeValidator{} + +// preferWriteOnlyAttributeValidator implements the validator. +type preferWriteOnlyAttributeValidator struct { + oldAttribute path.Expression + writeOnlyAttribute path.Expression +} + +// Description describes the validation in plain text formatting. +func (v preferWriteOnlyAttributeValidator) Description(ctx context.Context) string { + return fmt.Sprintf("The write-only attribute %s should be preferred over the regular attribute %s", v.writeOnlyAttribute, v.oldAttribute) +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (v preferWriteOnlyAttributeValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +// ValidateResource performs the validation. +func (v preferWriteOnlyAttributeValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + return + } + + oldAttributePaths, oldAttributeDiags := req.Config.PathMatches(ctx, v.oldAttribute) + if oldAttributeDiags.HasError() { + resp.Diagnostics.Append(oldAttributeDiags...) + return + } + + _, writeOnlyAttributeDiags := req.Config.PathMatches(ctx, v.writeOnlyAttribute) + if writeOnlyAttributeDiags.HasError() { + resp.Diagnostics.Append(writeOnlyAttributeDiags...) + return + } + + for _, mp := range oldAttributePaths { + // Get the value + var matchedValue attr.Value + diags := req.Config.GetAttribute(ctx, mp, &matchedValue) + resp.Diagnostics.Append(diags...) + if diags.HasError() { + continue + } + + if matchedValue.IsUnknown() { + return + } + + if matchedValue.IsNull() { + continue + } + + resp.Diagnostics.AddAttributeWarning(mp, + "Available Write-Only Attribute Alternative", + fmt.Sprintf("The attribute has a WriteOnly version %s available. "+ + "Use the WriteOnly version of the attribute when possible.", v.writeOnlyAttribute.String())) + } +} diff --git a/resourcevalidator/prefer_write_only_attribute_example_test.go b/resourcevalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..6e61bde6 --- /dev/null +++ b/resourcevalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resourcevalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + // Throws a warning diagnostic if the resource supports write-only + // attributes and the oldAttribute has a known value. + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("oldAttribute"), + path.MatchRoot("writeOnlyAttribute"), + ), + } +} diff --git a/resourcevalidator/prefer_write_only_attribute_test.go b/resourcevalidator/prefer_write_only_attribute_test.go new file mode 100644 index 00000000..2256e8ea --- /dev/null +++ b/resourcevalidator/prefer_write_only_attribute_test.go @@ -0,0 +1,152 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resourcevalidator_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" +) + +func TestPreferWriteOnlyAttribute(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + validators []resource.ConfigValidator + req resource.ValidateConfigRequest + expected *resource.ValidateConfigResponse + }{ + "valid-warning-diag": { + validators: []resource.ConfigValidator{ + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("oldAttribute1"), + path.MatchRoot("writeOnlyAttribute1"), + ), + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("oldAttribute2"), + path.MatchRoot("writeOnlyAttribute2"), + ), + }, + req: resource.ValidateConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "oldAttribute1": schema.StringAttribute{ + Optional: true, + }, + "writeOnlyAttribute1": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "oldAttribute2": schema.StringAttribute{ + Optional: true, + }, + "writeOnlyAttribute2": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "oldAttribute1": tftypes.String, + "writeOnlyAttribute1": tftypes.String, + "oldAttribute2": tftypes.String, + "writeOnlyAttribute2": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "oldAttribute1": tftypes.NewValue(tftypes.String, nil), + "writeOnlyAttribute1": tftypes.NewValue(tftypes.String, nil), + "oldAttribute2": tftypes.NewValue(tftypes.String, "test-value"), + "writeOnlyAttribute2": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic(path.Root("oldAttribute2"), + "Available Write-Only Attribute Alternative", + "The attribute has a WriteOnly version writeOnlyAttribute2 available. "+ + "Use the WriteOnly version of the attribute when possible."), + }, + }, + }, + "valid-no-client-capabilities": { + validators: []resource.ConfigValidator{ + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("oldAttribute1"), + path.MatchRoot("writeOnlyAttribute1"), + ), + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("oldAttribute2"), + path.MatchRoot("writeOnlyAttribute2"), + ), + }, + req: resource.ValidateConfigRequest{ + Config: tfsdk.Config{ + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "oldAttribute1": schema.StringAttribute{ + Optional: true, + }, + "writeOnlyAttribute1": schema.StringAttribute{ + Optional: true, + }, + "oldAttribute2": schema.StringAttribute{ + Optional: true, + }, + "writeOnlyAttribute2": schema.StringAttribute{ + Optional: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "oldAttribute1": tftypes.String, + "writeOnlyAttribute1": tftypes.String, + "oldAttribute2": tftypes.String, + "writeOnlyAttribute2": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "oldAttribute1": tftypes.NewValue(tftypes.String, nil), + "writeOnlyAttribute1": tftypes.NewValue(tftypes.String, nil), + "oldAttribute2": tftypes.NewValue(tftypes.String, "test-value"), + "writeOnlyAttribute2": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + }, + expected: &resource.ValidateConfigResponse{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &resource.ValidateConfigResponse{} + + resourcevalidator.AnyWithAllWarnings(testCase.validators...).ValidateResource(context.Background(), testCase.req, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/stringvalidator/prefer_write_only_attribute.go b/stringvalidator/prefer_write_only_attribute.go new file mode 100644 index 00000000..920546de --- /dev/null +++ b/stringvalidator/prefer_write_only_attribute.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringvalidator + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator" +) + +// PreferWriteOnlyAttribute returns a warning if the Terraform client supports +// write-only attributes, and the attribute that the validator is applied to has a value. +// It takes in a path.Expression that represents the write-only attribute schema location, +// and the warning message will indicate that the write-only attribute should be preferred. +// +// This validator should only be used for resource attributes as other schema types do not +// support write-only attributes. +// +// This implements the validation logic declaratively within the schema. +// Refer to [resourcevalidator.PreferWriteOnlyAttribute] +// for declaring this type of validation outside the schema definition. +func PreferWriteOnlyAttribute(writeOnlyAttribute path.Expression) validator.String { + return schemavalidator.PreferWriteOnlyAttribute{ + WriteOnlyAttribute: writeOnlyAttribute, + } +} diff --git a/stringvalidator/prefer_write_only_attribute_example_test.go b/stringvalidator/prefer_write_only_attribute_example_test.go new file mode 100644 index 00000000..5ecf8d0c --- /dev/null +++ b/stringvalidator/prefer_write_only_attribute_example_test.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package stringvalidator_test + +import ( + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" +) + +func ExamplePreferWriteOnlyAttribute() { + // Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "example_attr": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Throws a warning diagnostic encouraging practitioners to use + // write_only_attr if example_attr has a value. + stringvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("write_only_attr"), + ), + }, + }, + "write_only_attr": schema.StringAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +}