diff --git a/.changes/unreleased/FEATURES-20250206-114700.yaml b/.changes/unreleased/FEATURES-20250206-114700.yaml new file mode 100644 index 000000000..ce306a4c9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20250206-114700.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/schema: Added `WriteOnly` schema field for managed resource schemas to indicate a write-only attribute. +Write-only attribute values are not saved to the Terraform plan or state artifacts.' +time: 2025-02-06T11:47:00.176842-05:00 +custom: + Issue: "1044" diff --git a/.changes/unreleased/NOTES-20250206-114436.yaml b/.changes/unreleased/NOTES-20250206-114436.yaml new file mode 100644 index 000000000..0ce8a7603 --- /dev/null +++ b/.changes/unreleased/NOTES-20250206-114436.yaml @@ -0,0 +1,5 @@ +kind: NOTES +body: Write-only attribute support is in technical preview and offered without compatibility promises until Terraform 1.11 is generally available. +time: 2025-02-06T11:44:36.156747-05:00 +custom: + Issue: "1044" diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index b9f6e3820..1d984a8db 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,11 @@ func (a BoolAttribute) IsRequired() bool { return a.Required } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + // IsSensitive returns the Sensitive field value. func (a BoolAttribute) IsSensitive() bool { return a.Sensitive diff --git a/datasource/schema/bool_attribute_test.go b/datasource/schema/bool_attribute_test.go index 40e36a08d..b493d892d 100644 --- a/datasource/schema/bool_attribute_test.go +++ b/datasource/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 6b1b6c83e..4659cadf6 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -182,6 +183,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/datasource/schema/dynamic_attribute_test.go b/datasource/schema/dynamic_attribute_test.go index 8981dbecd..95fe01fb4 100644 --- a/datasource/schema/dynamic_attribute_test.go +++ b/datasource/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index 8f3dbdc21..d2510f5c3 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -189,3 +189,8 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go index 0da3cc94e..1040f6245 100644 --- a/datasource/schema/float32_attribute_test.go +++ b/datasource/schema/float32_attribute_test.go @@ -424,3 +424,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 1313353ec..1d893dd33 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/float64_attribute_test.go b/datasource/schema/float64_attribute_test.go index f2e05ebf7..b83e16abd 100644 --- a/datasource/schema/float64_attribute_test.go +++ b/datasource/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index 89f852e8c..d06a9b8ce 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -189,3 +189,8 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go index 3df85e9c7..23ce97fd5 100644 --- a/datasource/schema/int32_attribute_test.go +++ b/datasource/schema/int32_attribute_test.go @@ -424,3 +424,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index ab9d5ca1b..85bd4a445 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -188,3 +189,8 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/datasource/schema/int64_attribute_test.go b/datasource/schema/int64_attribute_test.go index c1ac3c7a4..b15eeb85f 100644 --- a/datasource/schema/int64_attribute_test.go +++ b/datasource/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 9d502067f..2e5140602 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -202,6 +203,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_attribute_test.go b/datasource/schema/list_attribute_test.go index 27294a130..1e7335ce3 100644 --- a/datasource/schema/list_attribute_test.go +++ b/datasource/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index b9b70d6fb..922320627 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -230,6 +231,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_nested_attribute_test.go b/datasource/schema/list_nested_attribute_test.go index 5d1e7db88..fd1cf6941 100644 --- a/datasource/schema/list_nested_attribute_test.go +++ b/datasource/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 516576a4e..3d6c57680 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -205,6 +206,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_attribute_test.go b/datasource/schema/map_attribute_test.go index d79425462..ab3d31167 100644 --- a/datasource/schema/map_attribute_test.go +++ b/datasource/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index 9729efdc3..9bf1bb957 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -231,6 +231,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_nested_attribute_test.go b/datasource/schema/map_nested_attribute_test.go index ea6dc480d..671f55593 100644 --- a/datasource/schema/map_nested_attribute_test.go +++ b/datasource/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index ffe4e0839..c21f74a15 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -185,6 +186,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/datasource/schema/number_attribute_test.go b/datasource/schema/number_attribute_test.go index f8cec323d..449dda7ce 100644 --- a/datasource/schema/number_attribute_test.go +++ b/datasource/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index eafa40c6e..a004329ac 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -204,6 +205,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/object_attribute_test.go b/datasource/schema/object_attribute_test.go index c517465ed..ab3c428ce 100644 --- a/datasource/schema/object_attribute_test.go +++ b/datasource/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -404,6 +405,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 261b02424..859b0558d 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -200,6 +201,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_attribute_test.go b/datasource/schema/set_attribute_test.go index 793095705..9a0760b9b 100644 --- a/datasource/schema/set_attribute_test.go +++ b/datasource/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 1b17b8743..26b14e449 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -226,6 +226,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_nested_attribute_test.go b/datasource/schema/set_nested_attribute_test.go index 7f7815f70..4b064ee7c 100644 --- a/datasource/schema/set_nested_attribute_test.go +++ b/datasource/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index 811e76de4..b6f4e7f32 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -240,6 +240,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/single_nested_attribute_test.go b/datasource/schema/single_nested_attribute_test.go index 25a5e565e..2a62a8e42 100644 --- a/datasource/schema/single_nested_attribute_test.go +++ b/datasource/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -530,6 +531,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index 0c2dd9aba..95534fece 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,6 +182,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not supported in data source schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/datasource/schema/string_attribute_test.go b/datasource/schema/string_attribute_test.go index d436b9bd1..f86110feb 100644 --- a/datasource/schema/string_attribute_test.go +++ b/datasource/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index 47e248aae..56790dee2 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -183,3 +184,9 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go index 077ee1f1f..7eebac422 100644 --- a/ephemeral/schema/bool_attribute_test.go +++ b/ephemeral/schema/bool_attribute_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -423,3 +424,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index 5088fef63..9cd22e70a 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -180,6 +181,12 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go index a718167a6..99fbf6f4e 100644 --- a/ephemeral/schema/dynamic_attribute_test.go +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -390,6 +390,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index 93ce2ac7d..46af5bae2 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -187,3 +187,9 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go index e9e45d785..f542972aa 100644 --- a/ephemeral/schema/float32_attribute_test.go +++ b/ephemeral/schema/float32_attribute_test.go @@ -424,3 +424,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index ff1912d1e..f4699e210 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -186,3 +187,9 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go index 4c3f2703a..db9e8db2b 100644 --- a/ephemeral/schema/float64_attribute_test.go +++ b/ephemeral/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat54AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index f98cbe3b0..76f6e0194 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -187,3 +187,9 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go index bd6d4d36c..e118cb0db 100644 --- a/ephemeral/schema/int32_attribute_test.go +++ b/ephemeral/schema/int32_attribute_test.go @@ -424,3 +424,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt2AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index 52b1b7ffd..27cb2dd23 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -186,3 +187,9 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go index e61165c5f..5f07ff2ff 100644 --- a/ephemeral/schema/int64_attribute_test.go +++ b/ephemeral/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -423,3 +424,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 3846316b6..258757f99 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -74,7 +75,6 @@ type ListAttribute struct { // Sensitive indicates whether the value of this attribute should be // considered sensitive data. Setting it to true will obscure the value - // in CLI output. Sensitive bool // Description is used in various tooling, like the language server, to @@ -200,6 +200,12 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go index 1b22bbbdc..4ab1985d8 100644 --- a/ephemeral/schema/list_attribute_test.go +++ b/ephemeral/schema/list_attribute_test.go @@ -521,3 +521,31 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index 4a91f2742..a4478fb48 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -228,6 +229,12 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go index 3d1ae651d..7e1153e79 100644 --- a/ephemeral/schema/list_nested_attribute_test.go +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -569,6 +569,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 366619ed5..0741747a4 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -203,6 +204,12 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go index 94219da83..7da8aa46f 100644 --- a/ephemeral/schema/map_attribute_test.go +++ b/ephemeral/schema/map_attribute_test.go @@ -398,6 +398,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index 11d701cc9..de057e106 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -229,6 +229,12 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go index 0e7986f89..ef21cf89b 100644 --- a/ephemeral/schema/map_nested_attribute_test.go +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -569,6 +569,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index 547396409..17d557398 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -183,6 +184,12 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/ephemeral/schema/number_attribute_test.go b/ephemeral/schema/number_attribute_test.go index 7e326b90e..d3636d006 100644 --- a/ephemeral/schema/number_attribute_test.go +++ b/ephemeral/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -390,6 +391,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index 1ff506f9d..fa808e4ba 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -76,7 +77,6 @@ type ObjectAttribute struct { // Sensitive indicates whether the value of this attribute should be // considered sensitive data. Setting it to true will obscure the value - // in CLI output. Sensitive bool // Description is used in various tooling, like the language server, to @@ -202,6 +202,12 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go index 429aba1e7..76f4aab38 100644 --- a/ephemeral/schema/object_attribute_test.go +++ b/ephemeral/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -438,6 +439,34 @@ func TestObjectAttributeObjectValidators(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index 7f6a408ed..7ecd08ffd 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,6 +199,12 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go index 0b6903829..a6211d666 100644 --- a/ephemeral/schema/set_attribute_test.go +++ b/ephemeral/schema/set_attribute_test.go @@ -398,6 +398,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index 7ed6d2fdf..658dc0df7 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -224,6 +224,12 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go index 1bd3daa65..bcfa627a4 100644 --- a/ephemeral/schema/set_nested_attribute_test.go +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -569,6 +570,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index a57db9c65..167b8c136 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -238,6 +238,12 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/single_nested_attribute_test.go b/ephemeral/schema/single_nested_attribute_test.go index 5b7209edb..d3690828f 100644 --- a/ephemeral/schema/single_nested_attribute_test.go +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -530,6 +530,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index 1b5c7db22..3cfefe97c 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -179,6 +180,12 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to ephemeral resource schemas, +// as these schemas describe data that is explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/ephemeral/schema/string_attribute_test.go b/ephemeral/schema/string_attribute_test.go index 1c95add7d..e0577b3cb 100644 --- a/ephemeral/schema/string_attribute_test.go +++ b/ephemeral/schema/string_attribute_test.go @@ -390,6 +390,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() diff --git a/internal/fromproto5/client_capabilities.go b/internal/fromproto5/client_capabilities.go index 0eaf40c50..737354888 100644 --- a/internal/fromproto5/client_capabilities.go +++ b/internal/fromproto5/client_capabilities.go @@ -89,3 +89,16 @@ func OpenEphemeralResourceClientCapabilities(in *tfprotov5.OpenEphemeralResource DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceTypeConfigClientCapabilities(in *tfprotov5.ValidateResourceTypeConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto5/validateresourcetypeconfig.go b/internal/fromproto5/validateresourcetypeconfig.go index ae6e41298..1919f9e9c 100644 --- a/internal/fromproto5/validateresourcetypeconfig.go +++ b/internal/fromproto5/validateresourcetypeconfig.go @@ -6,11 +6,12 @@ package fromproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // ValidateResourceTypeConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceTypeConfigRequest(ctx context.Context, proto5 *tfprotov5.Va fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceTypeConfigClientCapabilities(proto5.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto5/validateresourcetypeconfig_test.go b/internal/fromproto5/validateresourcetypeconfig_test.go index 8aec17098..5c07807ed 100644 --- a/internal/fromproto5/validateresourcetypeconfig_test.go +++ b/internal/fromproto5/validateresourcetypeconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "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/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceTypeConfigRequest(t *testing.T) { @@ -88,6 +89,39 @@ func TestValidateResourceTypeConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + ClientCapabilities: &tfprotov5.ValidateResourceTypeConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov5.ValidateResourceTypeConfigRequest{ + Config: &testProto5DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fromproto6/client_capabilities.go b/internal/fromproto6/client_capabilities.go index cd9c92b9c..d22d81623 100644 --- a/internal/fromproto6/client_capabilities.go +++ b/internal/fromproto6/client_capabilities.go @@ -89,3 +89,16 @@ func OpenEphemeralResourceClientCapabilities(in *tfprotov6.OpenEphemeralResource DeferralAllowed: in.DeferralAllowed, } } + +func ValidateResourceConfigClientCapabilities(in *tfprotov6.ValidateResourceConfigClientCapabilities) resource.ValidateConfigClientCapabilities { + if in == nil { + // Client did not indicate any supported capabilities + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + } + } + + return resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: in.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/fromproto6/validateresourceconfig.go b/internal/fromproto6/validateresourceconfig.go index 1eb65aa87..f3ea643c9 100644 --- a/internal/fromproto6/validateresourceconfig.go +++ b/internal/fromproto6/validateresourceconfig.go @@ -6,11 +6,12 @@ package fromproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // ValidateResourceConfigRequest returns the *fwserver.ValidateResourceConfigRequest @@ -26,6 +27,7 @@ func ValidateResourceConfigRequest(ctx context.Context, proto6 *tfprotov6.Valida fw.Config = config fw.Resource = resource + fw.ClientCapabilities = ValidateResourceConfigClientCapabilities(proto6.ClientCapabilities) return fw, diags } diff --git a/internal/fromproto6/validateresourceconfig_test.go b/internal/fromproto6/validateresourceconfig_test.go index 4e21d432c..f649a0d77 100644 --- a/internal/fromproto6/validateresourceconfig_test.go +++ b/internal/fromproto6/validateresourceconfig_test.go @@ -8,6 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -15,8 +18,6 @@ import ( "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/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestValidateResourceConfigRequest(t *testing.T) { @@ -88,6 +89,39 @@ func TestValidateResourceConfigRequest(t *testing.T) { }, }, }, + "client-capabilities": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + ClientCapabilities: &tfprotov6.ValidateResourceConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, + "client-capabilities-not-set": { + input: &tfprotov6.ValidateResourceConfigRequest{ + Config: &testProto6DynamicValue, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: false, + }, + Config: &tfsdk.Config{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index 4face5a95..6c440b319 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -4,8 +4,9 @@ package fwschema import ( - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" ) // Attribute is the core interface required for implementing Terraform @@ -63,6 +64,13 @@ type Attribute interface { // sensitive. This is named differently than Sensitive to prevent a // conflict with the tfsdk.Attribute field name. IsSensitive() bool + + // IsWriteOnly should return true if the attribute configuration value is + // write-only. This is named differently than WriteOnly to prevent a + // conflict with the tfsdk.Attribute field name. + // + // Write-only attributes are a managed-resource schema concept only. + IsWriteOnly() bool } // AttributesEqual is a helper function to perform equality testing on two @@ -101,5 +109,9 @@ func AttributesEqual(a, b Attribute) bool { return false } + if a.IsWriteOnly() != b.IsWriteOnly() { + return false + } + return true } diff --git a/internal/fwschema/write_only_nested_attribute_validation.go b/internal/fwschema/write_only_nested_attribute_validation.go new file mode 100644 index 000000000..4cba87413 --- /dev/null +++ b/internal/fwschema/write_only_nested_attribute_validation.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwschema + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// ContainsAllWriteOnlyChildAttributes will return true if all child attributes for the +// given nested attribute have WriteOnly set to true. +func ContainsAllWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + if !childAttr.IsWriteOnly() { + return false + } + + nestedAttribute, ok := childAttr.(NestedAttribute) + if ok { + if !ContainsAllWriteOnlyChildAttributes(nestedAttribute) { + return false + } + } + } + + return true +} + +// ContainsAnyWriteOnlyChildAttributes will return true if any child attribute for the +// given nested attribute has WriteOnly set to true. +func ContainsAnyWriteOnlyChildAttributes(nestedAttr NestedAttribute) bool { + nestedObjAttrs := nestedAttr.GetNestedObject().GetAttributes() + + for _, childAttr := range nestedObjAttrs { + if childAttr.IsWriteOnly() { + return true + } + + nestedAttribute, ok := childAttr.(NestedAttribute) + if ok { + if ContainsAnyWriteOnlyChildAttributes(nestedAttribute) { + return true + } + } + } + + return false +} + +func InvalidWriteOnlyNestedAttributeDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ) +} + +func InvalidComputedNestedAttributeWithWriteOnlyDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a Computed nested attribute that contains a WriteOnly child attribute.\n\n", attributePath)+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ) +} diff --git a/internal/fwschemadata/data_nullify_collection_blocks.go b/internal/fwschemadata/data_nullify_collection_blocks.go index f907d6d16..a0a19ecd4 100644 --- a/internal/fwschemadata/data_nullify_collection_blocks.go +++ b/internal/fwschemadata/data_nullify_collection_blocks.go @@ -7,11 +7,12 @@ import ( "context" "errors" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // NullifyCollectionBlocks converts list and set block empty values to null diff --git a/internal/fwserver/attribute_validation.go b/internal/fwserver/attribute_validation.go index 98b05f0fb..3c2492b14 100644 --- a/internal/fwserver/attribute_validation.go +++ b/internal/fwserver/attribute_validation.go @@ -34,6 +34,11 @@ type ValidateAttributeRequest struct { // Config contains the entire configuration of the data source, provider, or resource. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateAttributeResponse represents a response to a @@ -65,6 +70,24 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt return } + if a.IsWriteOnly() && a.IsRequired() && a.IsOptional() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ) + return + } + + if a.IsWriteOnly() && a.IsComputed() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ) + return + } + configData := &fwschemadata.Data{ Description: fwschemadata.DataDescriptionConfiguration, Schema: req.Config.Schema, @@ -106,6 +129,19 @@ func AttributeValidate(ctx context.Context, a fwschema.Attribute, req ValidateAt ) } + // If the client doesn't support write-only attributes (first supported in Terraform v1.11.0), then we raise an early validation error + // to avoid a confusing data consistency error when the provider attempts to return "null" for a write-only attribute in the planned/final state. + // + // Write-only attributes can only be successfully used with a supporting client, so the only option for a practitoner to utilize a write-only attribute + // is to upgrade their Terraform CLI version to v1.11.0 or later. + if !req.ClientCapabilities.WriteOnlyAttributesAllowed && a.IsWriteOnly() && !attributeConfig.IsNull() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "WriteOnly Attribute Not Allowed", + fmt.Sprintf("The resource contains a non-null value for WriteOnly attribute %s. Write-only attributes are only supported in Terraform 1.11 and later.", req.AttributePath.String()), + ) + } + req.AttributeConfig = attributeConfig switch attributeWithValidators := a.(type) { @@ -200,10 +236,11 @@ func AttributeValidateBool(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.BoolRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.BoolValidators() { @@ -265,10 +302,11 @@ func AttributeValidateFloat32(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float32Validators() { @@ -330,10 +368,11 @@ func AttributeValidateFloat64(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.Float64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Float64Validators() { @@ -395,10 +434,11 @@ func AttributeValidateInt32(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int32Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int32Validators() { @@ -460,10 +500,11 @@ func AttributeValidateInt64(ctx context.Context, attribute fwxschema.AttributeWi } validateReq := validator.Int64Request{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.Int64Validators() { @@ -525,10 +566,11 @@ func AttributeValidateList(ctx context.Context, attribute fwxschema.AttributeWit } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ListValidators() { @@ -590,10 +632,11 @@ func AttributeValidateMap(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.MapRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.MapValidators() { @@ -655,10 +698,11 @@ func AttributeValidateNumber(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.NumberRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.NumberValidators() { @@ -720,10 +764,11 @@ func AttributeValidateObject(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.ObjectValidators() { @@ -785,10 +830,11 @@ func AttributeValidateSet(ctx context.Context, attribute fwxschema.AttributeWith } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.SetValidators() { @@ -850,10 +896,11 @@ func AttributeValidateString(ctx context.Context, attribute fwxschema.AttributeW } validateReq := validator.StringRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.StringValidators() { @@ -915,10 +962,11 @@ func AttributeValidateDynamic(ctx context.Context, attribute fwxschema.Attribute } validateReq := validator.DynamicRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, attributeValidator := range attribute.DynamicValidators() { @@ -992,6 +1040,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1026,6 +1075,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1060,6 +1110,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath.AtMapKey(key), AttributePathExpression: req.AttributePathExpression.AtMapKey(key), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1097,6 +1148,7 @@ func AttributeValidateNestedAttributes(ctx context.Context, a fwschema.Attribute AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttributeObjectResp := &ValidateAttributeResponse{} @@ -1144,10 +1196,11 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { @@ -1182,6 +1235,7 @@ func NestedAttributeObjectValidate(ctx context.Context, o fwschema.NestedAttribu AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } nestedAttrResp := &ValidateAttributeResponse{} diff --git a/internal/fwserver/attribute_validation_test.go b/internal/fwserver/attribute_validation_test.go index 457264e0f..6f0fbc5ab 100644 --- a/internal/fwserver/attribute_validation_test.go +++ b/internal/fwserver/attribute_validation_test.go @@ -1700,6 +1700,275 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "write-only-attr-with-required": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-required-null-value": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Missing Configuration for Required Attribute", + "Must set a configuration value for the test attribute as the provider has marked it as required.\n\n"+ + "Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.", + ), + }, + }, + }, + "write-only-attr-with-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "write-only-attr-with-computed": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes cannot be set with Computed. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-missing-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "Attribute missing Required, Optional, or Computed definition. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-with-computed-required-and-optional": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + WriteOnly: true, + Required: true, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Definition", + "WriteOnly Attributes must be set with only one of Required or Optional. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "write-only-attr-set-no-client-capability": { + req: ValidateAttributeRequest{ + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + // Client indicating it doesn't support write-only attributes + WriteOnlyAttributesAllowed: false, + }, + AttributePath: path.Root("test"), + Config: tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "hello world!"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Required: true, + WriteOnly: true, + Type: types.StringType, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "WriteOnly Attribute Not Allowed", + "The resource contains a non-null value for WriteOnly attribute test. "+ + "Write-only attributes are only supported in Terraform 1.11 and later.", + ), + }, + }, + }, } for name, tc := range testCases { @@ -1786,6 +2055,32 @@ func TestAttributeValidateBool(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithBoolValidators{ + Validators: []validator.Bool{ + testvalidator.Bool{ + ValidateBoolMethod: func(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected BoolRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.BoolValue(true), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithBoolValidators{ Validators: []validator.Bool{ @@ -1990,6 +2285,32 @@ func TestAttributeValidateFloat32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat32Validators{ + Validators: []validator.Float32{ + testvalidator.Float32{ + ValidateFloat32Method: func(ctx context.Context, req validator.Float32Request, resp *validator.Float32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float32Value(0.1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat32Validators{ Validators: []validator.Float32{ @@ -2194,6 +2515,32 @@ func TestAttributeValidateFloat64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithFloat64Validators{ + Validators: []validator.Float64{ + testvalidator.Float64{ + ValidateFloat64Method: func(ctx context.Context, req validator.Float64Request, resp *validator.Float64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Float64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Float64Value(0.2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithFloat64Validators{ Validators: []validator.Float64{ @@ -2398,6 +2745,32 @@ func TestAttributeValidateInt32(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt32Validators{ + Validators: []validator.Int32{ + testvalidator.Int32{ + ValidateInt32Method: func(ctx context.Context, req validator.Int32Request, resp *validator.Int32Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int32Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int32Value(1), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt32Validators{ Validators: []validator.Int32{ @@ -2602,6 +2975,32 @@ func TestAttributeValidateInt64(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithInt64Validators{ + Validators: []validator.Int64{ + testvalidator.Int64{ + ValidateInt64Method: func(ctx context.Context, req validator.Int64Request, resp *validator.Int64Response) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected Int64Request.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.Int64Value(2), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithInt64Validators{ Validators: []validator.Int64{ @@ -2808,6 +3207,32 @@ func TestAttributeValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithListValidators{ + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithListValidators{ ElementType: types.StringType, @@ -3033,6 +3458,35 @@ func TestAttributeValidateMap(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithMapValidators{ + Validators: []validator.Map{ + testvalidator.Map{ + ValidateMapMethod: func(ctx context.Context, req validator.MapRequest, resp *validator.MapResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected MapRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.MapValueMust( + types.StringType, + map[string]attr.Value{"testkey": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithMapValidators{ ElementType: types.StringType, @@ -3262,6 +3716,32 @@ func TestAttributeValidateNumber(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithNumberValidators{ + Validators: []validator.Number{ + testvalidator.Number{ + ValidateNumberMethod: func(ctx context.Context, req validator.NumberRequest, resp *validator.NumberResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected NumberRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.NumberValue(big.NewFloat(1.2)), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithNumberValidators{ Validators: []validator.Number{ @@ -3478,6 +3958,35 @@ func TestAttributeValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithObjectValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("testvalue")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithObjectValidators{ AttributeTypes: map[string]attr.Type{ @@ -3715,6 +4224,32 @@ func TestAttributeValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithSetValidators{ + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithSetValidators{ ElementType: types.StringType, @@ -3932,6 +4467,32 @@ func TestAttributeValidateString(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithStringValidators{ + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.StringValue("testVal"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithStringValidators{ Validators: []validator.String{ @@ -4136,6 +4697,32 @@ func TestAttributeValidateDynamic(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + attribute: testschema.AttributeWithDynamicValidators{ + Validators: []validator.Dynamic{ + testvalidator.Dynamic{ + ValidateDynamicMethod: func(ctx context.Context, req validator.DynamicRequest, resp *validator.DynamicResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected DynamicRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.DynamicValue(types.StringValue("test")), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { attribute: testschema.AttributeWithDynamicValidators{ Validators: []validator.Dynamic{ @@ -4373,6 +4960,32 @@ func TestNestedAttributeObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedAttributeObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, + "request-config": { object: testschema.NestedAttributeObjectWithValidators{ Validators: []validator.Object{ diff --git a/internal/fwserver/block_validation.go b/internal/fwserver/block_validation.go index d65b1b5c7..fcf3898f8 100644 --- a/internal/fwserver/block_validation.go +++ b/internal/fwserver/block_validation.go @@ -76,6 +76,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtListIndex(idx), AttributePathExpression: req.AttributePathExpression.AtListIndex(idx), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -110,6 +111,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: value, AttributePath: req.AttributePath.AtSetValue(value), AttributePathExpression: req.AttributePathExpression.AtSetValue(value), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -143,6 +145,7 @@ func BlockValidate(ctx context.Context, b fwschema.Block, req ValidateAttributeR AttributeConfig: o, AttributePath: req.AttributePath, AttributePathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockObjectResp := &ValidateAttributeResponse{} @@ -203,10 +206,11 @@ func BlockValidateList(ctx context.Context, block fwxschema.BlockWithListValidat } validateReq := validator.ListRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ListValidators() { @@ -268,10 +272,11 @@ func BlockValidateObject(ctx context.Context, block fwxschema.BlockWithObjectVal } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.ObjectValidators() { @@ -333,10 +338,11 @@ func BlockValidateSet(ctx context.Context, block fwxschema.BlockWithSetValidator } validateReq := validator.SetRequest{ - Config: req.Config, - ConfigValue: configValue, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: configValue, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, blockValidator := range block.SetValidators() { @@ -395,10 +401,11 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject } validateReq := validator.ObjectRequest{ - Config: req.Config, - ConfigValue: object, - Path: req.AttributePath, - PathExpression: req.AttributePathExpression, + ClientCapabilities: req.ClientCapabilities, + Config: req.Config, + ConfigValue: object, + Path: req.AttributePath, + PathExpression: req.AttributePathExpression, } for _, objectValidator := range objectWithValidators.ObjectValidators() { @@ -432,6 +439,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedAttrReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedAttrResp := &ValidateAttributeResponse{} @@ -445,6 +453,7 @@ func NestedBlockObjectValidate(ctx context.Context, o fwschema.NestedBlockObject nestedBlockReq := ValidateAttributeRequest{ AttributePath: req.AttributePath.AtName(nestedName), AttributePathExpression: req.AttributePathExpression.AtName(nestedName), + ClientCapabilities: req.ClientCapabilities, Config: req.Config, } nestedBlockResp := &ValidateAttributeResponse{} diff --git a/internal/fwserver/block_validation_test.go b/internal/fwserver/block_validation_test.go index 367a0df1c..40726f22f 100644 --- a/internal/fwserver/block_validation_test.go +++ b/internal/fwserver/block_validation_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -20,7 +22,6 @@ import ( "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" ) func TestBlockValidate(t *testing.T) { @@ -746,6 +747,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "list-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeList, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "set-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -874,6 +946,77 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "set-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSet, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, "single-no-validation": { req: ValidateAttributeRequest{ AttributePath: path.Root("test"), @@ -976,6 +1119,64 @@ func TestBlockValidate(t *testing.T) { }, }, }, + "single-validation-client-capabilities": { + req: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{WriteOnlyAttributesAllowed: true}, + Config: tfsdk.Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + Schema: testschema.Schema{ + Blocks: map[string]fwschema.Block{ + "test": testschema.Block{ + NestedObject: testschema.NestedBlockObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_attr": testschema.AttributeWithStringValidators{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected StringRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + }, + }, + NestingMode: fwschema.BlockNestingModeSingle, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, } for name, tc := range testCases { @@ -1085,6 +1286,44 @@ func TestBlockValidateList(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithListValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.List{ + testvalidator.List{ + ValidateListMethod: func(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ListRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithListValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1402,6 +1641,37 @@ func TestBlockValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithObjectValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithObjectValidators{ Attributes: map[string]fwschema.Attribute{ @@ -1679,6 +1949,44 @@ func TestBlockValidateSet(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + block: testschema.BlockWithSetValidators{ + Attributes: map[string]fwschema.Attribute{ + "testattr": testschema.AttributeWithStringValidators{}, + }, + Validators: []validator.Set{ + testvalidator.Set{ + ValidateSetMethod: func(ctx context.Context, req validator.SetRequest, resp *validator.SetResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected SetRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{"testattr": types.StringType}, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{"testattr": types.StringType}, + map[string]attr.Value{"testattr": types.StringValue("test")}, + ), + }, + ), + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { block: testschema.BlockWithSetValidators{ Attributes: map[string]fwschema.Attribute{ @@ -2069,6 +2377,31 @@ func TestNestedBlockObjectValidateObject(t *testing.T) { response: &ValidateAttributeResponse{}, expected: &ValidateAttributeResponse{}, }, + "request-client-capabilities": { + object: testschema.NestedBlockObjectWithValidators{ + Validators: []validator.Object{ + testvalidator.Object{ + ValidateObjectMethod: func(ctx context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError( + "Unexpected ObjectRequest.ClientCapabilities", + "Missing WriteOnlyAttributesAllowed client capability", + ) + } + }, + }, + }, + }, + request: ValidateAttributeRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testAttributeConfig, + ClientCapabilities: validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: true, + }, + }, + response: &ValidateAttributeResponse{}, + expected: &ValidateAttributeResponse{}, + }, "request-config": { object: testschema.NestedBlockObjectWithValidators{ Validators: []validator.Object{ diff --git a/internal/fwserver/schema_validation.go b/internal/fwserver/schema_validation.go index 32c50f9d4..dedce9d43 100644 --- a/internal/fwserver/schema_validation.go +++ b/internal/fwserver/schema_validation.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -20,6 +21,11 @@ type ValidateSchemaRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities validator.ValidateSchemaClientCapabilities } // ValidateSchemaResponse represents a response to a @@ -43,6 +49,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. @@ -58,6 +65,7 @@ func SchemaValidate(ctx context.Context, s fwschema.Schema, req ValidateSchemaRe AttributePath: path.Root(name), AttributePathExpression: path.MatchRoot(name), Config: req.Config, + ClientCapabilities: req.ClientCapabilities, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 59e10d76c..41ab43c77 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -59,6 +59,31 @@ func TestServerApplyResourceChange(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_optional_write_only": tftypes.String, + "test_required_write_only": tftypes.String, + }, + } + + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_optional_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required_write_only": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + } + + type testSchemaDataWriteOnly struct { + TestOptionalWriteOnly types.String `tfsdk:"test_optional_write_only"` + TestRequiredWriteOnly types.String `tfsdk:"test_required_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -398,6 +423,53 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newstate-write-only": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: testEmptyState, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "create-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1259,6 +1331,59 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, "old-optional-value"), + "test_required_write_only": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaWriteOnly, + }, + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_optional_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "update-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index 30c491690..d5a0aef2e 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -156,11 +156,21 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index d3e83c31b..988a268c9 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -33,6 +33,13 @@ func TestServerCreateResource(t *testing.T) { }, } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -76,6 +83,18 @@ func TestServerCreateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -91,6 +110,11 @@ func TestServerCreateResource(t *testing.T) { TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -506,6 +530,39 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 7288cc3e7..4abe204cd 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -142,6 +142,18 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta return } + // Set any write-only attributes in the import state to null + modifiedState, err := tftypes.Transform(importResp.State.Raw, NullifyWriteOnlyAttributes(ctx, importResp.State.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Import State", + "There was an unexpected error modifying the Import State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + importResp.State.Raw = modifiedState + if importResp.State.Raw.Equal(req.EmptyState.Raw) { resp.Diagnostics.AddError( "Missing Resource Import State", diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index bd0275a12..d605ef3d9 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -33,12 +33,26 @@ func TestServerImportResourceState(t *testing.T) { }, } + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "write-only": tftypes.String, + "required": tftypes.String, + }, + } + testEmptyStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, nil), "optional": tftypes.NewValue(tftypes.String, nil), "required": tftypes.NewValue(tftypes.String, nil), }) + testEmptyStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, nil), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }) + testUnknownStateValue := tftypes.NewValue(testType, tftypes.UnknownValue) testStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ @@ -61,11 +75,31 @@ func TestServerImportResourceState(t *testing.T) { }, } + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required": schema.StringAttribute{ + Required: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: testEmptyStateValue, Schema: testSchema, } + testEmptyStateWriteOnly := &tfsdk.State{ + Raw: testEmptyStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testUnknownState := &tfsdk.State{ Raw: testUnknownStateValue, Schema: testSchema, @@ -416,6 +450,39 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyStateWriteOnly, + ID: "test-id", + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("write-only"), "write-only-val")...) + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id"), + "write-only": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWriteOnly, + }, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_moveresourcestate.go b/internal/fwserver/server_moveresourcestate.go index 480e4a956..1a832f3fe 100644 --- a/internal/fwserver/server_moveresourcestate.go +++ b/internal/fwserver/server_moveresourcestate.go @@ -205,6 +205,18 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe return } + // Set any write-only attributes in the move resource state to null + modifiedState, err := tftypes.Transform(moveStateResp.TargetState.Raw, NullifyWriteOnlyAttributes(ctx, moveStateResp.TargetState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Move Resource State", + "There was an unexpected error modifying the Move Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + moveStateResp.TargetState.Raw = modifiedState + // If the implement has set the state in any way, return the response. if !moveStateResp.TargetState.Raw.Equal(tftypes.NewValue(req.TargetResourceSchema.Type().TerraformType(ctx), nil)) { resp.Diagnostics = moveStateResp.Diagnostics diff --git a/internal/fwserver/server_moveresourcestate_test.go b/internal/fwserver/server_moveresourcestate_test.go index 1a8e0fb77..931b84a76 100644 --- a/internal/fwserver/server_moveresourcestate_test.go +++ b/internal/fwserver/server_moveresourcestate_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerMoveResourceState(t *testing.T) { @@ -40,6 +41,22 @@ func TestServerMoveResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.MoveResourceStateRequest @@ -757,6 +774,44 @@ func TestServerMoveResourceState(t *testing.T) { }, }, }, + "response-TargetState-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "write_only_attribute": nil, + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("write_only_attribute"), "movestate-val")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchemaWriteOnly, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index d66fc4897..aacd2c998 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromtftypes" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -338,6 +339,18 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange "Ensure all resource plan modifiers do not attempt to change resource plan data from being a null value if the request plan is a null value.", ) } + + // Set any write-only attributes in the plan to null + modifiedPlan, err := tftypes.Transform(resp.PlannedState.Raw, NullifyWriteOnlyAttributes(ctx, resp.PlannedState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Planned State", + "There was an unexpected error modifying the PlannedState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + + resp.PlannedState.Raw = modifiedPlan } func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { @@ -500,6 +513,67 @@ func NormaliseRequiresReplace(ctx context.Context, rs path.Paths) path.Paths { return ret[:j] } +// RequiredWriteOnlyNilsAttributePaths returns a tftypes.Walk() function +// that populates reqWriteOnlyNilsPaths with the paths of Required and WriteOnly +// attributes that have a null value. +func RequiredWriteOnlyNilsAttributePaths(ctx context.Context, schema fwschema.Schema, reqWriteOnlyNilsPaths *path.Paths) func(path *tftypes.AttributePath, value tftypes.Value) (bool, error) { + return func(attrPath *tftypes.AttributePath, value tftypes.Value) (bool, error) { + // we are only modifying attributes, not the entire resource + if len(attrPath.Steps()) < 1 { + return true, nil + } + + ctx = logging.FrameworkWithAttributePath(ctx, attrPath.String()) + + attribute, err := schema.AttributeAtTerraformPath(ctx, attrPath) + + if err != nil { + if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { + // atomic attributes can be nested block objects that contain child writeOnly attributes + return true, nil + } + + if errors.Is(err, fwschema.ErrPathIsBlock) { + // blocks do not have the writeOnly field but can contain child writeOnly attributes + return true, nil + } + + if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { + return false, nil + } + + logging.FrameworkError(ctx, "couldn't find attribute in resource schema") + return false, fmt.Errorf("couldn't find attribute in resource schema: %w", err) + } + + if attribute.IsWriteOnly() { + if attribute.IsRequired() && value.IsNull() { + fwPath, diags := fromtftypes.AttributePath(ctx, attrPath, schema) + if diags.HasError() { + for _, diagErr := range diags.Errors() { + logging.FrameworkError(ctx, + "Error converting tftypes.AttributePath to path.Path", + map[string]any{ + logging.KeyError: diagErr.Detail(), + }, + ) + } + + return false, fmt.Errorf("couldn't convert tftypes.AttributePath to path.Path") + } + reqWriteOnlyNilsPaths.Append(fwPath) + + // if the value is nil, there is no need to continue walking + return false, nil + } + // check for any writeOnly child attributes + return true, nil + } + + return false, nil + } +} + // planToState returns a *tfsdk.State with a copied value from a tfsdk.Plan. func planToState(plan tfsdk.Plan) *tfsdk.State { return &tfsdk.State{ diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index 66e693935..4e22b36c6 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "math/big" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -363,6 +364,305 @@ func TestMarkComputedNilsAsUnknown(t *testing.T) { } } +func TestRequiredWriteOnlyNilsAttributePath(t *testing.T) { + t.Parallel() + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string-value": schema.StringAttribute{ + Required: true, + }, + "string-nil-optional-writeonly": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-value-optional-writeonly": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-nil-required-writeonly": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-value-required-writeonly": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "list-value": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + }, + "list-nil-optional-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + WriteOnly: true, + }, + "list-value-optional-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + WriteOnly: true, + }, + "list-nil-required-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + WriteOnly: true, + }, + "list-value-required-writeonly": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + WriteOnly: true, + }, + "dynamic-value": schema.DynamicAttribute{ + Required: true, + }, + "dynamic-nil-optional-writeonly": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-optional-writeonly": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-nil-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + "dynamic-value-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + // underlying values of dynamic attributes should be left alone + "dynamic-value-with-underlying-list-required-writeonly": schema.DynamicAttribute{ + Required: true, + WriteOnly: true, + }, + "object-nil-required-writeonly": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "object-value-required-writeonly": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Required: true, + WriteOnly: true, + }, + "nested-nil-required-writeonly": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Required: true, + WriteOnly: true, + }, + "nested-value-required-writeonly": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Required: true, + WriteOnly: true, + }, + "optional-nested-value-required-writeonly-attributes": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "block-nil-required-writeonly-attributes": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + "block-value-required-writeonly-attributes": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil-optional-writeonly": tftypes.NewValue(tftypes.String, nil), + "string-value-optional-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil-required-writeonly": tftypes.NewValue(tftypes.String, nil), + "string-value-required-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "list-value": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), + "list-nil-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + "list-value-optional-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{tftypes.NewValue(tftypes.String, "hello, world")}), + "list-nil-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, nil), + "list-value-required-writeonly": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "hello, world"), + tftypes.NewValue(tftypes.String, nil), + }), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil-optional-writeonly": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-optional-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil-required-writeonly": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-required-writeonly": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value-with-underlying-list-required-writeonly": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Bool, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, nil), + }, + ), + "object-nil-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "foo"), + }), + "nested-nil-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-required-writeonly": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "optional-nested-value-required-writeonly-attributes": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "block-nil-required-writeonly-attributes": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-required-writeonly-attributes": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + }), + }) + expected := path.Paths{ + path.Root("block-value-required-writeonly-attributes"). + AtSetValue(types.ObjectValueMust( + map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + map[string]attr.Value{ + "string-nil": types.StringNull(), + "string-set": types.StringValue("bar"), + }, + )). + AtName("string-nil"), + path.Root("dynamic-nil-required-writeonly"), + path.Root("list-nil-required-writeonly"), + path.Root("nested-value-required-writeonly").AtName("string-nil"), + path.Root("object-nil-required-writeonly"), + path.Root("optional-nested-value-required-writeonly-attributes").AtName("string-nil"), + path.Root("string-nil-required-writeonly"), + path.Root("nested-nil-required-writeonly"), + } + + var got path.Paths + err := tftypes.Walk(input, fwserver.RequiredWriteOnlyNilsAttributePaths(context.Background(), s, &got)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].String() < got[j].String() + }) + + sort.Slice(expected, func(i, j int) bool { + return expected[i].String() < expected[j].String() + }) + + if diff := cmp.Diff(got, expected, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } +} + func TestNormaliseRequiresReplace(t *testing.T) { t.Parallel() diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 628a2e445..d260e2912 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -6,6 +6,8 @@ package fwserver import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -157,11 +159,21 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index a9520edd4..f6711551a 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -34,11 +34,23 @@ func TestServerReadResource(t *testing.T) { }, } + testTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_write_only": tftypes.String, + "test_required": tftypes.String, + }, + } + testCurrentStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testCurrentStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }) + testNewStateValue := tftypes.NewValue(testType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, "test-newstate-value"), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), @@ -55,6 +67,18 @@ func TestServerReadResource(t *testing.T) { }, } + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -97,6 +121,11 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testCurrentStateWriteOnly := &tfsdk.State{ + Raw: testCurrentStateValueWriteOnly, + Schema: testSchemaWriteOnly, + } + testNewState := &tfsdk.State{ Raw: testNewStateValue, Schema: testSchema, @@ -562,6 +591,38 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-state-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentStateWriteOnly, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data struct { + TestWriteOnly types.String `tfsdk:"test_write_only"` + TestRequired types.String `tfsdk:"test_required"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + data.TestWriteOnly = types.StringValue("test-write-only-value") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ + "test_write_only": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), + }), + Schema: testSchemaWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 9112c35c2..ad1d9f998 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -169,11 +169,21 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } - if semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { - return + if !semanticEqualityResp.NewData.TerraformValue.Equal(resp.NewState.Raw) { + logging.FrameworkDebug(ctx, "State updated due to semantic equality") + + resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue } - logging.FrameworkDebug(ctx, "State updated due to semantic equality") + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(resp.NewState.Raw, NullifyWriteOnlyAttributes(ctx, resp.NewState.Schema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying State", + "There was an unexpected error modifying the NewState. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } - resp.NewState.Raw = semanticEqualityResp.NewData.TerraformValue + resp.NewState.Raw = modifiedState } diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 6ad7a38d6..5c0bb5c7d 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -34,6 +34,13 @@ func TestServerUpdateResource(t *testing.T) { }, } + testSchemaTypeWriteOnly := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_required": tftypes.String, + "test_write_only": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -77,6 +84,18 @@ func TestServerUpdateResource(t *testing.T) { }, } + testSchemaWithWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_required": schema.StringAttribute{ + Required: true, + }, + "test_write_only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -87,6 +106,11 @@ func TestServerUpdateResource(t *testing.T) { TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` } + type testSchemaDataWriteOnly struct { + TestRequired types.String `tfsdk:"test_required"` + TestWriteOnly types.String `tfsdk:"test_write_only"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -777,6 +801,39 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newstate-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, "test-write-only-value"), + }), + Schema: testSchemaWithWriteOnly, + }, + ResourceSchema: testSchemaWithWriteOnly, + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data testSchemaDataWriteOnly + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaTypeWriteOnly, map[string]tftypes.Value{ + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_write_only": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchemaWithWriteOnly, + }, + Private: testEmptyPrivate, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_upgraderesourcestate.go b/internal/fwserver/server_upgraderesourcestate.go index b2cc340e5..536c3dfd0 100644 --- a/internal/fwserver/server_upgraderesourcestate.go +++ b/internal/fwserver/server_upgraderesourcestate.go @@ -225,9 +225,19 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradedStateValue, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + resp.UpgradedState = &tfsdk.State{ Schema: req.ResourceSchema, - Raw: upgradedStateValue, + Raw: modifiedState, } return @@ -243,5 +253,27 @@ func (s *Server) UpgradeResourceState(ctx context.Context, req *UpgradeResourceS return } + // Set any write-only attributes in the state to null + modifiedState, err := tftypes.Transform(upgradeResourceStateResponse.State.Raw, NullifyWriteOnlyAttributes(ctx, req.ResourceSchema)) + if err != nil { + resp.Diagnostics.AddError( + "Error Modifying Upgraded Resource State", + "There was an unexpected error modifying the Upgraded Resource State. This is always a problem with the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + upgradeResourceStateResponse.State.Raw = modifiedState + + // If the write-only nullification results in a null state, then this is a provider error + if upgradeResourceStateResponse.State.Raw.Type() == nil || upgradeResourceStateResponse.State.Raw.IsNull() { + resp.Diagnostics.AddError( + "Missing Upgraded Resource State", + fmt.Sprintf("After attempting a resource state upgrade to version %d, the provider did not return any state data. ", req.Version)+ + "Preventing the unexpected loss of resource state data. "+ + "This is always an issue with the Terraform Provider and should be reported to the provider developer.", + ) + return + } + resp.UpgradedState = &upgradeResourceStateResponse.State } diff --git a/internal/fwserver/server_upgraderesourcestate_test.go b/internal/fwserver/server_upgraderesourcestate_test.go index b720bed7e..1e733874f 100644 --- a/internal/fwserver/server_upgraderesourcestate_test.go +++ b/internal/fwserver/server_upgraderesourcestate_test.go @@ -42,6 +42,23 @@ func TestServerUpgradeResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "write_only_attribute": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "required_attribute": schema.StringAttribute{ + Required: true, + }, + }, + Version: 1, // Must be above 0 + } + schemaTypeWriteOnly := testSchemaWriteOnly.Type().TerraformType(ctx) + testCases := map[string]struct { server *fwserver.Server request *fwserver.UpgradeResourceStateRequest @@ -342,6 +359,71 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "RawState-DynamicValue-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var rawState struct { + Id string `json:"id"` + RequiredAttribute bool `json:"required_attribute"` + } + + if err := json.Unmarshal(req.RawState.JSON, &rawState); err != nil { + resp.Diagnostics.AddError( + "Unable to Unmarshal Prior State", + err.Error(), + ) + return + } + + dynamicValue, err := tfprotov6.NewDynamicValue( + schemaTypeWriteOnly, + tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, rawState.Id), + "write_only_attribute": tftypes.NewValue(tftypes.String, "write-only-dynamic-value"), + "required_attribute": tftypes.NewValue(tftypes.String, fmt.Sprintf("%t", rawState.RequiredAttribute)), + }), + ) + + if err != nil { + resp.Diagnostics.AddError( + "Unable to Create Upgraded State", + err.Error(), + ) + return + } + + resp.DynamicValue = &dynamicValue + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "ResourceType-UpgradeState-not-implemented": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -517,6 +599,72 @@ func TestServerUpgradeResourceState(t *testing.T) { }, }, }, + "PriorSchema-and-State-write-only-nullification": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpgradeResourceStateRequest{ + RawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + ResourceSchema: testSchemaWriteOnly, + Resource: &testprovider.ResourceWithUpgradeState{ + Resource: &testprovider.Resource{}, + UpgradeStateMethod: func(ctx context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + PriorSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "required_attribute": schema.BoolAttribute{ + Required: true, + }, + }, + }, + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + var priorStateData struct { + Id string `tfsdk:"id"` + RequiredAttribute bool `tfsdk:"required_attribute"` + } + + resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) + + if resp.Diagnostics.HasError() { + return + } + + upgradedStateData := struct { + Id string `tfsdk:"id"` + WriteOnlyAttribute string `tfsdk:"write_only_attribute"` + RequiredAttribute string `tfsdk:"required_attribute"` + }{ + Id: priorStateData.Id, + WriteOnlyAttribute: "write-only-upgraded-state", + RequiredAttribute: fmt.Sprintf("%t", priorStateData.RequiredAttribute), + } + + resp.Diagnostics.Append(resp.State.Set(ctx, upgradedStateData)...) + }, + }, + } + }, + }, + Version: 0, + }, + expectedResponse: &fwserver.UpgradeResourceStateResponse{ + UpgradedState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaTypeWriteOnly, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "write_only_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchemaWriteOnly, + }, + }, + }, "PriorSchema-and-State-json-mismatch": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_validatedatasourceconfig.go b/internal/fwserver/server_validatedatasourceconfig.go index 3379b15ad..33653982d 100644 --- a/internal/fwserver/server_validatedatasourceconfig.go +++ b/internal/fwserver/server_validatedatasourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateDataSourceConfig(ctx context.Context, req *ValidateData resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateephemeralresourceconfig.go b/internal/fwserver/server_validateephemeralresourceconfig.go index a99a0dbfb..6956af068 100644 --- a/internal/fwserver/server_validateephemeralresourceconfig.go +++ b/internal/fwserver/server_validateephemeralresourceconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -96,8 +97,17 @@ func (s *Server) ValidateEphemeralResourceConfig(ctx context.Context, req *Valid resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateproviderconfig.go b/internal/fwserver/server_validateproviderconfig.go index 588f021c2..0109e6e07 100644 --- a/internal/fwserver/server_validateproviderconfig.go +++ b/internal/fwserver/server_validateproviderconfig.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -77,8 +78,17 @@ func (s *Server) ValidateProviderConfig(ctx context.Context, req *ValidateProvid resp.Diagnostics.Append(vpcRes.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + // The SchemaValidate function is shared between provider, resource, + // data source and ephemeral resource schemas; however, WriteOnlyAttributesAllowed + // capability is only valid for resource schemas, so this is explicitly set to false + // for all other schema types. + WriteOnlyAttributesAllowed: false, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateresourceconfig.go b/internal/fwserver/server_validateresourceconfig.go index 79e8ae9b7..591ce5a2f 100644 --- a/internal/fwserver/server_validateresourceconfig.go +++ b/internal/fwserver/server_validateresourceconfig.go @@ -9,14 +9,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) // ValidateResourceConfigRequest is the framework server request for the // ValidateResourceConfig RPC. type ValidateResourceConfigRequest struct { - Config *tfsdk.Config - Resource resource.Resource + ClientCapabilities resource.ValidateConfigClientCapabilities + Config *tfsdk.Config + Resource resource.Resource } // ValidateResourceConfigResponse is the framework server response for the @@ -51,7 +53,8 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour } vdscReq := resource.ValidateConfigRequest{ - Config: *req.Config, + ClientCapabilities: req.ClientCapabilities, + Config: *req.Config, } if resourceWithConfigValidators, ok := req.Resource.(resource.ResourceWithConfigValidators); ok { @@ -96,8 +99,13 @@ func (s *Server) ValidateResourceConfig(ctx context.Context, req *ValidateResour resp.Diagnostics.Append(vdscResp.Diagnostics...) } + schemaCapabilities := validator.ValidateSchemaClientCapabilities{ + WriteOnlyAttributesAllowed: req.ClientCapabilities.WriteOnlyAttributesAllowed, + } + validateSchemaReq := ValidateSchemaRequest{ - Config: *req.Config, + ClientCapabilities: schemaCapabilities, + Config: *req.Config, } // Instantiate a new response for each request to prevent validators // from modifying or removing diagnostics. diff --git a/internal/fwserver/server_validateresourceconfig_test.go b/internal/fwserver/server_validateresourceconfig_test.go index 859612031..489d6a234 100644 --- a/internal/fwserver/server_validateresourceconfig_test.go +++ b/internal/fwserver/server_validateresourceconfig_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -18,7 +20,6 @@ import ( "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" ) func TestServerValidateResourceConfig(t *testing.T) { @@ -47,6 +48,10 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchema, } + testClientCapabilities := resource.ValidateConfigClientCapabilities{ + WriteOnlyAttributesAllowed: true, + } + testSchemaAttributeValidator := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -69,6 +74,28 @@ func TestServerValidateResourceConfig(t *testing.T) { Schema: testSchemaAttributeValidator, } + testSchemaAttributeValidatorClientCapabilities := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + testvalidator.String{ + ValidateStringMethod: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + }, + }, + } + + testConfigAttributeValidatorClientCapabilities := tfsdk.Config{ + Raw: testValue, + Schema: testSchemaAttributeValidatorClientCapabilities, + } + testSchemaAttributeValidatorError := schema.Schema{ Attributes: map[string]schema.Attribute{ "test": schema.StringAttribute{ @@ -128,6 +155,21 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-AttributeValidator-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfigAttributeValidatorClientCapabilities, + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchemaAttributeValidatorClientCapabilities + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-AttributeValidator-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -185,6 +227,34 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithConfigValidators-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithConfigValidators{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ConfigValidatorsMethod: func(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + &testprovider.ResourceConfigValidator{ + ValidateResourceMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithConfigValidators-diagnostics": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -259,6 +329,28 @@ func TestServerValidateResourceConfig(t *testing.T) { }, expectedResponse: &fwserver.ValidateResourceConfigResponse{}, }, + "request-config-ResourceWithValidateConfig-client-capabilities": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ValidateResourceConfigRequest{ + ClientCapabilities: testClientCapabilities, + Config: &testConfig, + Resource: &testprovider.ResourceWithValidateConfig{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + }, + ValidateConfigMethod: func(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + resp.Diagnostics.AddError("Incorrect req.ClientCapabilities", "expected WriteOnlyAttributesAllowed client capability") + } + }, + }, + }, + expectedResponse: &fwserver.ValidateResourceConfigResponse{}, + }, "request-config-ResourceWithValidateConfig-diagnostic": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/write_only_nullification.go b/internal/fwserver/write_only_nullification.go new file mode 100644 index 000000000..5bfc77609 --- /dev/null +++ b/internal/fwserver/write_only_nullification.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// NullifyWriteOnlyAttributes transforms a tftypes.Value, setting all write-only attribute values +// to null according to the given managed resource schema. This function is called in all managed +// resource RPCs before a response is sent to Terraform Core. Terraform Core expects all write-only +// attribute values to be null to prevent data consistency errors. This can technically be done +// manually by the provider developers, but the Framework is handling it instead for convenience. +func NullifyWriteOnlyAttributes(ctx context.Context, resourceSchema fwschema.Schema) func(*tftypes.AttributePath, tftypes.Value) (tftypes.Value, error) { + return func(path *tftypes.AttributePath, val tftypes.Value) (tftypes.Value, error) { + ctx = logging.FrameworkWithAttributePath(ctx, path.String()) + + // we are only modifying attributes, not the entire resource + if len(path.Steps()) < 1 { + return val, nil + } + + attribute, err := resourceSchema.AttributeAtTerraformPath(ctx, path) + + if err != nil { + if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) { + // ignore attributes/elements inside schema.Attributes, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is a non-schema attribute, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathIsBlock) { + // ignore blocks, they do not have a writeOnly field + logging.FrameworkTrace(ctx, "attribute is a block, not nullifying") + return val, nil + } + + if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) { + // ignore attributes/elements inside schema.DynamicAttribute, they have no schema of their own + logging.FrameworkTrace(ctx, "attribute is inside of a dynamic attribute, not nullifying") + return val, nil + } + + logging.FrameworkError(ctx, "couldn't find attribute in resource schema") + + return tftypes.Value{}, fmt.Errorf("couldn't find attribute in resource schema: %w", err) + } + + // Value type from new state to create null with + newValueType := attribute.GetType().TerraformType(ctx) + + // If the attribute is dynamic set the new value type to DynamicPseudoType + // instead of the underlying concrete type + // TODO: verify if this is the correct behavior once Terraform Core implementation is complete + _, isDynamic := attribute.GetType().(basetypes.DynamicTypable) + if isDynamic { + newValueType = tftypes.DynamicPseudoType + } + + if attribute.IsWriteOnly() && !val.IsNull() { + logging.FrameworkDebug(ctx, "Nullifying write-only attribute in the newState") + + return tftypes.NewValue(newValueType, nil), nil + } + + return val, nil + } +} diff --git a/internal/fwserver/write_only_nullification_test.go b/internal/fwserver/write_only_nullification_test.go new file mode 100644 index 000000000..7aba06585 --- /dev/null +++ b/internal/fwserver/write_only_nullification_test.go @@ -0,0 +1,1710 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNullifyWriteOnlyAttributes(t *testing.T) { + t.Parallel() + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string-value": schema.StringAttribute{ + Required: true, + }, + "string-nil": schema.StringAttribute{ + Optional: true, + }, + "string-nil-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-value-write-only": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value": schema.DynamicAttribute{ + Required: true, + }, + "dynamic-nil": schema.DynamicAttribute{ + Optional: true, + }, + "dynamic-underlying-string-nil-computed": schema.DynamicAttribute{ + WriteOnly: true, + }, + "dynamic-nil-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "dynamic-value-with-underlying-list-write-only": schema.DynamicAttribute{ + Optional: true, + WriteOnly: true, + }, + "object-nil-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "object-value-write-only": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "string-nil": types.StringType, + "string-set": types.StringType, + }, + Optional: true, + WriteOnly: true, + }, + "nested-nil-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "nested-value-write-only": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "block-nil-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "block-value-write-only": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string-nil": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "string-set": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.String, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Bool, + }, + []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, true), + tftypes.NewValue(tftypes.Bool, false), + }, + ), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "foo"), + }), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, "bar"), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "string-value": tftypes.NewValue(tftypes.String, "hello, world"), + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-nil-write-only": tftypes.NewValue(tftypes.String, nil), + "string-value-write-only": tftypes.NewValue(tftypes.String, nil), + "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), + "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-nil-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "dynamic-value-with-underlying-list-write-only": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + "object-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "object-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-nil-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "nested-value-write-only": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, nil), + "block-nil-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, nil), + "block-value-write-only": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string-nil": tftypes.String, + "string-set": tftypes.String, + }, + }, map[string]tftypes.Value{ + "string-nil": tftypes.NewValue(tftypes.String, nil), + "string-set": tftypes.NewValue(tftypes.String, nil), + }), + }), + }) + + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} + +func TestNullifyWriteOnlyAttributes_NestedTypes(t *testing.T) { + t.Parallel() + nestedObjectType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + }, + } + + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + "map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-map-nested-attribute": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-map-nested-attribute-wo": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + "list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + "set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-single-nested-block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-single-nested-attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + }, + "nested-single-nested-attribute-wo": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + "list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-list-nested-block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-list-nested-attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-list-nested-attribute-wo": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + "set-nested-block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "nested-set-nested-block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "nested-set-nested-attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + }, + "nested-set-nested-attribute-wo": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested-string": schema.StringAttribute{ + Optional: true, + }, + "nested-string-wo": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + } + input := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "set-nested-attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + }), + }), + "set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, "foo-wo"), + }), + }), + }), + }), + }), + }), + }) + expected := tftypes.NewValue(s.Type().TerraformType(context.Background()), map[string]tftypes.Value{ + "single-nested-attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + "single-nested-attribute-wo": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, nil), + "map-nested-attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-map-nested-attribute": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, map[string]tftypes.Value{ + "keyA": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ElementType: nestedObjectType}, nil), + }), + }), + "map-nested-attribute-wo": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-map-nested-attribute": tftypes.Map{ElementType: nestedObjectType}, + "nested-map-nested-attribute-wo": tftypes.Map{ElementType: nestedObjectType}, + }, + }, + }, nil), + "list-nested-attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + "list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, nil), + "set-nested-attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + }), + }), + "set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, nil), + "single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + "nested-single-nested-block": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + "nested-single-nested-block": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-single-nested-attribute": nestedObjectType, + "nested-single-nested-attribute-wo": nestedObjectType, + }, + }, + map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-single-nested-attribute": tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + "nested-single-nested-attribute-wo": tftypes.NewValue(nestedObjectType, nil), + }), + }), + "list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + "nested-list-nested-block": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-list-nested-attribute": tftypes.List{ElementType: nestedObjectType}, + "nested-list-nested-attribute-wo": tftypes.List{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-list-nested-attribute": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-list-nested-attribute-wo": tftypes.NewValue(tftypes.List{ElementType: nestedObjectType}, nil), + }), + }), + }), + }), + "set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + "nested-set-nested-block": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested-string": tftypes.String, + "nested-string-wo": tftypes.String, + "nested-set-nested-attribute": tftypes.Set{ElementType: nestedObjectType}, + "nested-set-nested-attribute-wo": tftypes.Set{ElementType: nestedObjectType}, + }, + }, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + "nested-set-nested-attribute": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, []tftypes.Value{ + tftypes.NewValue(nestedObjectType, map[string]tftypes.Value{ + "nested-string": tftypes.NewValue(tftypes.String, "foo"), + "nested-string-wo": tftypes.NewValue(tftypes.String, nil), + }), + }), + "nested-set-nested-attribute-wo": tftypes.NewValue(tftypes.Set{ElementType: nestedObjectType}, nil), + }), + }), + }), + }), + }) + got, err := tftypes.Transform(input, NullifyWriteOnlyAttributes(context.Background(), s)) + if err != nil { + t.Errorf("Unexpected error: %s", err) + return + } + + diff, err := expected.Diff(got) + if err != nil { + t.Errorf("Error diffing values: %s", err) + return + } + for _, valDiff := range diff { + t.Errorf("Unexpected diff at path %v: expected: %v, got: %v", valDiff.Path, valDiff.Value1, valDiff.Value2) + } +} diff --git a/internal/proto6server/server_validateresourceconfig_test.go b/internal/proto6server/server_validateresourceconfig_test.go index 11e911745..36ea3f91c 100644 --- a/internal/proto6server/server_validateresourceconfig_test.go +++ b/internal/proto6server/server_validateresourceconfig_test.go @@ -8,12 +8,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerValidateResourceConfig(t *testing.T) { diff --git a/internal/testing/testschema/attribute.go b/internal/testing/testschema/attribute.go index fccc26b42..979db68eb 100644 --- a/internal/testing/testschema/attribute.go +++ b/internal/testing/testschema/attribute.go @@ -4,9 +4,10 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwschema.Attribute = Attribute{} @@ -19,6 +20,7 @@ type Attribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -77,3 +79,8 @@ func (a Attribute) IsRequired() bool { func (a Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a Attribute) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithbooldefault.go b/internal/testing/testschema/attributewithbooldefault.go index 5d7b25671..66edc07e5 100644 --- a/internal/testing/testschema/attributewithbooldefault.go +++ b/internal/testing/testschema/attributewithbooldefault.go @@ -22,6 +22,7 @@ type AttributeWithBoolDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Bool } @@ -85,3 +86,8 @@ func (a AttributeWithBoolDefaultValue) IsRequired() bool { func (a AttributeWithBoolDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go index d27661894..397f18186 100644 --- a/internal/testing/testschema/attributewithboolplanmodifiers.go +++ b/internal/testing/testschema/attributewithboolplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolPlanModifiers = AttributeWithBoolPlanModifiers{} @@ -22,6 +23,7 @@ type AttributeWithBoolPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Bool } @@ -85,3 +87,8 @@ func (a AttributeWithBoolPlanModifiers) IsRequired() bool { func (a AttributeWithBoolPlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go index 5cc0943ca..044c25cf4 100644 --- a/internal/testing/testschema/attributewithboolvalidators.go +++ b/internal/testing/testschema/attributewithboolvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithBoolValidators = AttributeWithBoolValidators{} @@ -22,6 +23,7 @@ type AttributeWithBoolValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Bool } @@ -85,3 +87,8 @@ func (a AttributeWithBoolValidators) IsRequired() bool { func (a AttributeWithBoolValidators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicdefault.go b/internal/testing/testschema/attributewithdynamicdefault.go index d366beb9a..b562132a8 100644 --- a/internal/testing/testschema/attributewithdynamicdefault.go +++ b/internal/testing/testschema/attributewithdynamicdefault.go @@ -22,6 +22,7 @@ type AttributeWithDynamicDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Dynamic } @@ -85,3 +86,8 @@ func (a AttributeWithDynamicDefaultValue) IsRequired() bool { func (a AttributeWithDynamicDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicplanmodifiers.go b/internal/testing/testschema/attributewithdynamicplanmodifiers.go index abb7ca6bf..74a80587a 100644 --- a/internal/testing/testschema/attributewithdynamicplanmodifiers.go +++ b/internal/testing/testschema/attributewithdynamicplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithDynamicPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Dynamic } @@ -85,3 +86,8 @@ func (a AttributeWithDynamicPlanModifiers) IsSensitive() bool { func (a AttributeWithDynamicPlanModifiers) DynamicPlanModifiers() []planmodifier.Dynamic { return a.PlanModifiers } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithdynamicvalidators.go b/internal/testing/testschema/attributewithdynamicvalidators.go index 1fe086775..e4ef1024e 100644 --- a/internal/testing/testschema/attributewithdynamicvalidators.go +++ b/internal/testing/testschema/attributewithdynamicvalidators.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithDynamicValidators = AttributeWithDynamicValidators{} @@ -22,6 +23,7 @@ type AttributeWithDynamicValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Dynamic } @@ -85,3 +87,8 @@ func (a AttributeWithDynamicValidators) IsSensitive() bool { func (a AttributeWithDynamicValidators) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go index e1dabdd87..c3aeb627d 100644 --- a/internal/testing/testschema/attributewithfloat32default.go +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -22,6 +22,7 @@ type AttributeWithFloat32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float32 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat32DefaultValue) IsRequired() bool { func (a AttributeWithFloat32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go index b3ef10ebc..f93d87229 100644 --- a/internal/testing/testschema/attributewithfloat32planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithFloat32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float32 } @@ -86,3 +87,8 @@ func (a AttributeWithFloat32PlanModifiers) IsRequired() bool { func (a AttributeWithFloat32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go index d9d38f9ea..7fb02a5ad 100644 --- a/internal/testing/testschema/attributewithfloat32validators.go +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -23,6 +23,7 @@ type AttributeWithFloat32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float32 } @@ -86,3 +87,8 @@ func (a AttributeWithFloat32Validators) IsRequired() bool { func (a AttributeWithFloat32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64default.go b/internal/testing/testschema/attributewithfloat64default.go index 33b717f33..484ec37d9 100644 --- a/internal/testing/testschema/attributewithfloat64default.go +++ b/internal/testing/testschema/attributewithfloat64default.go @@ -22,6 +22,7 @@ type AttributeWithFloat64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64DefaultValue) IsRequired() bool { func (a AttributeWithFloat64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go index 6273cbd5d..ba04291da 100644 --- a/internal/testing/testschema/attributewithfloat64planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithFloat64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64PlanModifiers) IsRequired() bool { func (a AttributeWithFloat64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go index b2b2b5f70..02ef17704 100644 --- a/internal/testing/testschema/attributewithfloat64validators.go +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -22,6 +22,7 @@ type AttributeWithFloat64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Float64 } @@ -85,3 +86,8 @@ func (a AttributeWithFloat64Validators) IsRequired() bool { func (a AttributeWithFloat64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go index bbfe22af8..f332bae41 100644 --- a/internal/testing/testschema/attributewithint32default.go +++ b/internal/testing/testschema/attributewithint32default.go @@ -22,6 +22,7 @@ type AttributeWithInt32DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int32 } @@ -85,3 +86,8 @@ func (a AttributeWithInt32DefaultValue) IsRequired() bool { func (a AttributeWithInt32DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go index aff453d9c..7f131df58 100644 --- a/internal/testing/testschema/attributewithint32planmodifiers.go +++ b/internal/testing/testschema/attributewithint32planmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithInt32PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int32 } @@ -86,3 +87,8 @@ func (a AttributeWithInt32PlanModifiers) IsRequired() bool { func (a AttributeWithInt32PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go index 7c6913bcc..8a4546e9e 100644 --- a/internal/testing/testschema/attributewithint32validators.go +++ b/internal/testing/testschema/attributewithint32validators.go @@ -23,6 +23,7 @@ type AttributeWithInt32Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int32 } @@ -86,3 +87,8 @@ func (a AttributeWithInt32Validators) IsRequired() bool { func (a AttributeWithInt32Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64default.go b/internal/testing/testschema/attributewithint64default.go index ca9e12b96..574a88a58 100644 --- a/internal/testing/testschema/attributewithint64default.go +++ b/internal/testing/testschema/attributewithint64default.go @@ -22,6 +22,7 @@ type AttributeWithInt64DefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64DefaultValue) IsRequired() bool { func (a AttributeWithInt64DefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go index 368a865c0..e21a43251 100644 --- a/internal/testing/testschema/attributewithint64planmodifiers.go +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithInt64PlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64PlanModifiers) IsRequired() bool { func (a AttributeWithInt64PlanModifiers) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go index 07cf28bd7..c4e23e166 100644 --- a/internal/testing/testschema/attributewithint64validators.go +++ b/internal/testing/testschema/attributewithint64validators.go @@ -22,6 +22,7 @@ type AttributeWithInt64Validators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Int64 } @@ -85,3 +86,8 @@ func (a AttributeWithInt64Validators) IsRequired() bool { func (a AttributeWithInt64Validators) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithlistdefault.go b/internal/testing/testschema/attributewithlistdefault.go index 1f6c65d5a..ff23c5215 100644 --- a/internal/testing/testschema/attributewithlistdefault.go +++ b/internal/testing/testschema/attributewithlistdefault.go @@ -23,6 +23,7 @@ type AttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.List } @@ -88,3 +89,8 @@ func (a AttributeWithListDefaultValue) IsRequired() bool { func (a AttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go index 08ed953fa..fbb50c332 100644 --- a/internal/testing/testschema/attributewithlistplanmodifiers.go +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithListPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.List } @@ -84,6 +85,11 @@ func (a AttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a AttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go index bb47ca9d6..fefa2eb02 100644 --- a/internal/testing/testschema/attributewithlistvalidators.go +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -23,6 +23,7 @@ type AttributeWithListValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.List } @@ -84,6 +85,11 @@ func (a AttributeWithListValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // ListValidators satisfies the fwxschema.AttributeWithListValidators interface. func (a AttributeWithListValidators) ListValidators() []validator.List { return a.Validators diff --git a/internal/testing/testschema/attributewithmapdefault.go b/internal/testing/testschema/attributewithmapdefault.go index a8bf1910d..2b223cd1d 100644 --- a/internal/testing/testschema/attributewithmapdefault.go +++ b/internal/testing/testschema/attributewithmapdefault.go @@ -23,6 +23,7 @@ type AttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Map } @@ -88,3 +89,8 @@ func (a AttributeWithMapDefaultValue) IsRequired() bool { func (a AttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go index 1d067e1d4..06c7b2fe4 100644 --- a/internal/testing/testschema/attributewithmapplanmodifiers.go +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithMapPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Map } @@ -84,6 +85,11 @@ func (a AttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a AttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go index 1469ceee2..5e3a9dac8 100644 --- a/internal/testing/testschema/attributewithmapvalidators.go +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -23,6 +23,7 @@ type AttributeWithMapValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Map } @@ -84,6 +85,11 @@ func (a AttributeWithMapValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // MapValidators satisfies the fwxschema.AttributeWithMapValidators interface. func (a AttributeWithMapValidators) MapValidators() []validator.Map { return a.Validators diff --git a/internal/testing/testschema/attributewithnumberdefault.go b/internal/testing/testschema/attributewithnumberdefault.go index 7bca51169..effa79507 100644 --- a/internal/testing/testschema/attributewithnumberdefault.go +++ b/internal/testing/testschema/attributewithnumberdefault.go @@ -22,6 +22,7 @@ type AttributeWithNumberDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Number } @@ -85,3 +86,8 @@ func (a AttributeWithNumberDefaultValue) IsRequired() bool { func (a AttributeWithNumberDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go index 7073a1e72..cf9299778 100644 --- a/internal/testing/testschema/attributewithnumberplanmodifiers.go +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithNumberPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberPlanModifiers satisfies the fwxschema.AttributeWithNumberPlanModifiers interface. func (a AttributeWithNumberPlanModifiers) NumberPlanModifiers() []planmodifier.Number { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithnumbervalidators.go b/internal/testing/testschema/attributewithnumbervalidators.go index 0e63b7db3..af1869d38 100644 --- a/internal/testing/testschema/attributewithnumbervalidators.go +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -22,6 +22,7 @@ type AttributeWithNumberValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Number } @@ -81,6 +82,11 @@ func (a AttributeWithNumberValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberValidators satisfies the fwxschema.AttributeWithNumberValidators interface. func (a AttributeWithNumberValidators) NumberValidators() []validator.Number { return a.Validators diff --git a/internal/testing/testschema/attributewithobjectdefault.go b/internal/testing/testschema/attributewithobjectdefault.go index 54e0e594e..ed25e0a90 100644 --- a/internal/testing/testschema/attributewithobjectdefault.go +++ b/internal/testing/testschema/attributewithobjectdefault.go @@ -23,6 +23,7 @@ type AttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Object } @@ -88,3 +89,8 @@ func (a AttributeWithObjectDefaultValue) IsRequired() bool { func (a AttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go index 87f5933e2..e4ec023bd 100644 --- a/internal/testing/testschema/attributewithobjectplanmodifiers.go +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -23,6 +23,7 @@ type AttributeWithObjectPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a AttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithobjectvalidators.go b/internal/testing/testschema/attributewithobjectvalidators.go index 854be0dbd..534c47cf6 100644 --- a/internal/testing/testschema/attributewithobjectvalidators.go +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -23,6 +23,7 @@ type AttributeWithObjectValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Object } @@ -84,6 +85,11 @@ func (a AttributeWithObjectValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectValidators satisfies the fwxschema.AttributeWithObjectValidators interface. func (a AttributeWithObjectValidators) ObjectValidators() []validator.Object { return a.Validators diff --git a/internal/testing/testschema/attributewithsetdefault.go b/internal/testing/testschema/attributewithsetdefault.go index 351ce17b8..f770f956c 100644 --- a/internal/testing/testschema/attributewithsetdefault.go +++ b/internal/testing/testschema/attributewithsetdefault.go @@ -23,6 +23,7 @@ type AttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.Set } @@ -88,3 +89,8 @@ func (a AttributeWithSetDefaultValue) IsRequired() bool { func (a AttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go index 4efb38ac4..a36f5adc4 100644 --- a/internal/testing/testschema/attributewithsetplanmodifiers.go +++ b/internal/testing/testschema/attributewithsetplanmodifiers.go @@ -4,12 +4,13 @@ package testschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) var _ fwxschema.AttributeWithSetPlanModifiers = AttributeWithSetPlanModifiers{} @@ -23,6 +24,7 @@ type AttributeWithSetPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.Set } @@ -84,6 +86,11 @@ func (a AttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a AttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithsetvalidators.go b/internal/testing/testschema/attributewithsetvalidators.go index 217b11b21..32bc5256f 100644 --- a/internal/testing/testschema/attributewithsetvalidators.go +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -23,6 +23,7 @@ type AttributeWithSetValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.Set } @@ -84,6 +85,11 @@ func (a AttributeWithSetValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // SetValidators satisfies the fwxschema.AttributeWithSetValidators interface. func (a AttributeWithSetValidators) SetValidators() []validator.Set { return a.Validators diff --git a/internal/testing/testschema/attributewithstringdefault.go b/internal/testing/testschema/attributewithstringdefault.go index 88e93f004..01e1e6f4e 100644 --- a/internal/testing/testschema/attributewithstringdefault.go +++ b/internal/testing/testschema/attributewithstringdefault.go @@ -22,6 +22,7 @@ type AttributeWithStringDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Default defaults.String } @@ -85,3 +86,8 @@ func (a AttributeWithStringDefaultValue) IsRequired() bool { func (a AttributeWithStringDefaultValue) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go index cbf324d66..d1b5af8d5 100644 --- a/internal/testing/testschema/attributewithstringplanmodifiers.go +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -22,6 +22,7 @@ type AttributeWithStringPlanModifiers struct { Optional bool Required bool Sensitive bool + WriteOnly bool PlanModifiers []planmodifier.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // StringPlanModifiers satisfies the fwxschema.AttributeWithStringPlanModifiers interface. func (a AttributeWithStringPlanModifiers) StringPlanModifiers() []planmodifier.String { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithstringvalidators.go b/internal/testing/testschema/attributewithstringvalidators.go index a864dd314..e1ddf7e59 100644 --- a/internal/testing/testschema/attributewithstringvalidators.go +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -22,6 +22,7 @@ type AttributeWithStringValidators struct { Optional bool Required bool Sensitive bool + WriteOnly bool Validators []validator.String } @@ -81,6 +82,11 @@ func (a AttributeWithStringValidators) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsWriteOnly() bool { + return a.WriteOnly +} + // StringValidators satisfies the fwxschema.AttributeWithStringValidators interface. func (a AttributeWithStringValidators) StringValidators() []validator.String { return a.Validators diff --git a/internal/testing/testschema/nested_attribute.go b/internal/testing/testschema/nested_attribute.go index f078f7cb9..f37301359 100644 --- a/internal/testing/testschema/nested_attribute.go +++ b/internal/testing/testschema/nested_attribute.go @@ -27,6 +27,7 @@ type NestedAttribute struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -157,3 +158,8 @@ func (a NestedAttribute) IsRequired() bool { func (a NestedAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} diff --git a/internal/testing/testschema/nested_attribute_with_list_default.go b/internal/testing/testschema/nested_attribute_with_list_default.go index 0980629cd..a1b70cd87 100644 --- a/internal/testing/testschema/nested_attribute_with_list_default.go +++ b/internal/testing/testschema/nested_attribute_with_list_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithListDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithListDefaultValue) ListDefaultValue() defaults.List { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go index e20d89df8..2904c21ee 100644 --- a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithListPlanModifiers struct { PlanModifiers []planmodifier.List Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithListPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a NestedAttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_map_default.go b/internal/testing/testschema/nested_attribute_with_map_default.go index 128ae137c..6eac9d3ff 100644 --- a/internal/testing/testschema/nested_attribute_with_map_default.go +++ b/internal/testing/testschema/nested_attribute_with_map_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithMapDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithMapDefaultValue) MapDefaultValue() defaults.Map { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go index 27ba35b45..35f2a0732 100644 --- a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithMapPlanModifiers struct { PlanModifiers []planmodifier.Map Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithMapPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a NestedAttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_object_default.go b/internal/testing/testschema/nested_attribute_with_object_default.go index 0f6a96489..e8579c2ac 100644 --- a/internal/testing/testschema/nested_attribute_with_object_default.go +++ b/internal/testing/testschema/nested_attribute_with_object_default.go @@ -28,6 +28,7 @@ type NestedAttributeWithObjectDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -105,6 +106,11 @@ func (a NestedAttributeWithObjectDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithObjectDefaultValue) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go index 00f303165..2d86af989 100644 --- a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go @@ -26,6 +26,7 @@ type NestedAttributeWithObjectPlanModifiers struct { PlanModifiers []planmodifier.Object Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -99,6 +100,11 @@ func (a NestedAttributeWithObjectPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a NestedAttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_set_default.go b/internal/testing/testschema/nested_attribute_with_set_default.go index a8f39e9a6..3a80c28a7 100644 --- a/internal/testing/testschema/nested_attribute_with_set_default.go +++ b/internal/testing/testschema/nested_attribute_with_set_default.go @@ -27,6 +27,7 @@ type NestedAttributeWithSetDefaultValue struct { Optional bool Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetDefaultValue) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithSetDefaultValue) SetDefaultValue() defaults.Set { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go index 6c988d86a..1d886e70e 100644 --- a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go @@ -27,6 +27,7 @@ type NestedAttributeWithSetPlanModifiers struct { PlanModifiers []planmodifier.Set Required bool Sensitive bool + WriteOnly bool Type attr.Type } @@ -102,6 +103,11 @@ func (a NestedAttributeWithSetPlanModifiers) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsWriteOnly() bool { + return a.WriteOnly +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a NestedAttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index 7a6cd4761..104814f5d 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -1338,6 +1370,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "ephemeral-resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + EphemeralResourceSchemas: map[string]fwschema.Schema{ + "test_ephemeral_resource": ephemeralschema.Schema{ + Attributes: map[string]ephemeralschema.Attribute{ + "test_attribute": ephemeralschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ + "test_ephemeral_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "ephemeral-resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ EphemeralResourceSchemas: map[string]fwschema.Schema{ @@ -2461,6 +2525,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4042,6 +4135,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Optional: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto5/schema_attribute.go b/internal/toproto5/schema_attribute.go index 74d8fa551..c9bc37e3a 100644 --- a/internal/toproto5/schema_attribute.go +++ b/internal/toproto5/schema_attribute.go @@ -6,9 +6,10 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov5.SchemaAttribute equivalent of an @@ -34,6 +35,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index 2df173187..161ae67a3 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -278,6 +278,38 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "data-source-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{ + "test_data_source": datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test_attribute": datasourceschema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{ + "test_data_source": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "test_attribute", + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + Functions: map[string]*tfprotov6.Function{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "data-source-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ DataSourceSchemas: map[string]fwschema.Schema{ @@ -2526,6 +2558,35 @@ func TestGetProviderSchemaResponse(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, + "provider-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "test_attribute": providerschema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_attribute", + Optional: true, + WriteOnly: false, + Type: tftypes.Bool, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, "provider-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{ @@ -4183,6 +4244,39 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + "resource-attribute-write-only": { + input: &fwserver.GetProviderSchemaResponse{ + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test_attribute": resourceschema.BoolAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + EphemeralResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_resource": { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Optional: true, + Name: "test_attribute", + WriteOnly: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + }, "resource-attribute-type-bool": { input: &fwserver.GetProviderSchemaResponse{ ResourceSchemas: map[string]fwschema.Schema{ diff --git a/internal/toproto6/schema_attribute.go b/internal/toproto6/schema_attribute.go index 492a5a2ab..d020a95d2 100644 --- a/internal/toproto6/schema_attribute.go +++ b/internal/toproto6/schema_attribute.go @@ -7,9 +7,10 @@ import ( "context" "sort" - "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) // SchemaAttribute returns the *tfprotov6.SchemaAttribute equivalent of an @@ -27,6 +28,7 @@ func SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePa Computed: a.IsComputed(), Sensitive: a.IsSensitive(), Type: a.GetType().TerraformType(ctx), + WriteOnly: a.IsWriteOnly(), } if a.GetDeprecationMessage() != "" { diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 6d96a516a..374296341 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,9 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go index 617225b54..50b3e06bb 100644 --- a/provider/metaschema/bool_attribute_test.go +++ b/provider/metaschema/bool_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index 8a4478655..ac2b79b0a 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,9 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go index 71cb2f688..f6f5ebd7f 100644 --- a/provider/metaschema/float64_attribute_test.go +++ b/provider/metaschema/float64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index 8751d574e..aeccd7030 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -120,3 +121,9 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go index 28efcebd7..2aa4d585e 100644 --- a/provider/metaschema/int64_attribute_test.go +++ b/provider/metaschema/int64_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index a3ff30e65..187d9c47c 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -134,6 +135,12 @@ func (a ListAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/list_attribute_test.go b/provider/metaschema/list_attribute_test.go index 6c864e8d6..424c1d602 100644 --- a/provider/metaschema/list_attribute_test.go +++ b/provider/metaschema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index a6b4f875a..0fa1b8221 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,9 @@ func (a ListNestedAttribute) IsRequired() bool { func (a ListNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go index dc6188822..c2aeae811 100644 --- a/provider/metaschema/list_nested_attribute_test.go +++ b/provider/metaschema/list_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index 51ee02edb..9103231ff 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -137,6 +138,12 @@ func (a MapAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/map_attribute_test.go b/provider/metaschema/map_attribute_test.go index 0956d8cc4..52ee19318 100644 --- a/provider/metaschema/map_attribute_test.go +++ b/provider/metaschema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 47a3b4348..587c56c0a 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -159,3 +160,9 @@ func (a MapNestedAttribute) IsRequired() bool { func (a MapNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go index 7c320b1bf..88eb68b26 100644 --- a/provider/metaschema/map_nested_attribute_test.go +++ b/provider/metaschema/map_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 86e45b8ab..511e7000a 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -121,3 +122,9 @@ func (a NumberAttribute) IsRequired() bool { func (a NumberAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go index 819f2d2a1..588dd4e0b 100644 --- a/provider/metaschema/number_attribute_test.go +++ b/provider/metaschema/number_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestNumberAttributeIsSensitive(t *testing.T) { }) } } + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index aa4c67be9..aabe40d4c 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -136,6 +137,12 @@ func (a ObjectAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/object_attribute_test.go b/provider/metaschema/object_attribute_test.go index e24db5dd1..f439faf0a 100644 --- a/provider/metaschema/object_attribute_test.go +++ b/provider/metaschema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -385,6 +386,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index 919713075..f7d3e4112 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -132,6 +133,12 @@ func (a SetAttribute) IsSensitive() bool { return false } +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/set_attribute_test.go b/provider/metaschema/set_attribute_test.go index 8cb5a95d5..620209b6f 100644 --- a/provider/metaschema/set_attribute_test.go +++ b/provider/metaschema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -18,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -379,6 +380,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index 233866a00..a3c6fbf9e 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -154,3 +155,9 @@ func (a SetNestedAttribute) IsRequired() bool { func (a SetNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go index 623fa3668..d0d86508c 100644 --- a/provider/metaschema/set_nested_attribute_test.go +++ b/provider/metaschema/set_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -546,3 +547,31 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index 0ed1a22fd..160fb1c80 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -6,11 +6,12 @@ package metaschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,3 +175,9 @@ func (a SingleNestedAttribute) IsRequired() bool { func (a SingleNestedAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go index f7c7cbda5..f04d55634 100644 --- a/provider/metaschema/single_nested_attribute_test.go +++ b/provider/metaschema/single_nested_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -510,3 +511,31 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 3a14d0721..fe25c5014 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -4,11 +4,12 @@ package metaschema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -117,3 +118,9 @@ func (a StringAttribute) IsRequired() bool { func (a StringAttribute) IsSensitive() bool { return false } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider meta schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go index 544fa268a..00f34d5b2 100644 --- a/provider/metaschema/string_attribute_test.go +++ b/provider/metaschema/string_attribute_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -370,3 +371,31 @@ func TestStringAttributeIsSensitive(t *testing.T) { }) } } + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index c411062e0..0502821ca 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,3 +179,9 @@ func (a BoolAttribute) IsRequired() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/bool_attribute_test.go b/provider/schema/bool_attribute_test.go index 043f2a896..50e4af6ed 100644 --- a/provider/schema/bool_attribute_test.go +++ b/provider/schema/bool_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestBoolAttributeIsSensitive(t *testing.T) { }) } } + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index c738d348d..4b31279f8 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -175,3 +176,9 @@ func (a DynamicAttribute) IsSensitive() bool { func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a DynamicAttribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/dynamic_attribute_test.go b/provider/schema/dynamic_attribute_test.go index c06eaf811..0779fe460 100644 --- a/provider/schema/dynamic_attribute_test.go +++ b/provider/schema/dynamic_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDynamicAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index a36c5c435..8e62dc96d 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,9 @@ func (a Float32Attribute) IsRequired() bool { func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go index e779b4a0a..107216df6 100644 --- a/provider/schema/float32_attribute_test.go +++ b/provider/schema/float32_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index 786965e3a..f0c0cc008 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,9 @@ func (a Float64Attribute) IsRequired() bool { func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/float64_attribute_test.go b/provider/schema/float64_attribute_test.go index 96604d0e7..b08486708 100644 --- a/provider/schema/float64_attribute_test.go +++ b/provider/schema/float64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { }) } } + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 16ff58f0f..1f8c60c39 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -182,3 +182,9 @@ func (a Int32Attribute) IsRequired() bool { func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go index b3ac0687c..a612fb42f 100644 --- a/provider/schema/int32_attribute_test.go +++ b/provider/schema/int32_attribute_test.go @@ -418,3 +418,31 @@ func TestInt32AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 3fd9713f1..25243bbf1 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -181,3 +182,9 @@ func (a Int64Attribute) IsRequired() bool { func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } + +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} diff --git a/provider/schema/int64_attribute_test.go b/provider/schema/int64_attribute_test.go index f5e457663..42efcaed4 100644 --- a/provider/schema/int64_attribute_test.go +++ b/provider/schema/int64_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -417,3 +418,31 @@ func TestInt64AttributeIsSensitive(t *testing.T) { }) } } + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index e733b297f..b85848b58 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -195,6 +196,12 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_attribute_test.go b/provider/schema/list_attribute_test.go index 3baa9382b..2a529ae13 100644 --- a/provider/schema/list_attribute_test.go +++ b/provider/schema/list_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 0c82da4ad..700299c05 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -14,7 +16,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -223,6 +224,12 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ListNestedAttribute) IsWriteOnly() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_nested_attribute_test.go b/provider/schema/list_nested_attribute_test.go index 43d76a612..33d6f1723 100644 --- a/provider/schema/list_nested_attribute_test.go +++ b/provider/schema/list_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestListNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index 77dc2b61d..82b5a05d7 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -198,6 +199,12 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_attribute_test.go b/provider/schema/map_attribute_test.go index 0bbef5f25..8c5548e6d 100644 --- a/provider/schema/map_attribute_test.go +++ b/provider/schema/map_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 2eed2fa08..14fb4092b 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -223,6 +223,12 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a MapNestedAttribute) IsWriteOnly() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_nested_attribute_test.go b/provider/schema/map_nested_attribute_test.go index 193960869..129321f22 100644 --- a/provider/schema/map_nested_attribute_test.go +++ b/provider/schema/map_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestMapNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index f3e90e2b7..bb6ffc6d6 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -178,6 +179,12 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/provider/schema/number_attribute_test.go b/provider/schema/number_attribute_test.go index b957cc73f..306e975e5 100644 --- a/provider/schema/number_attribute_test.go +++ b/provider/schema/number_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index 3041f5a79..c5c81a1ba 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -197,6 +198,12 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a ObjectAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/object_attribute_test.go b/provider/schema/object_attribute_test.go index 01f65b864..089d03f71 100644 --- a/provider/schema/object_attribute_test.go +++ b/provider/schema/object_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestObjectAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -398,6 +399,34 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index 3297452b7..eaf73344e 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -6,6 +6,8 @@ package schema import ( "context" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" @@ -13,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -193,6 +194,12 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_attribute_test.go b/provider/schema/set_attribute_test.go index 862b2d8dd..42aec99d1 100644 --- a/provider/schema/set_attribute_test.go +++ b/provider/schema/set_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -392,6 +393,34 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index 7a2fb6060..a9dad64a7 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -219,6 +219,12 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SetNestedAttribute) IsWriteOnly() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_nested_attribute_test.go b/provider/schema/set_nested_attribute_test.go index 163604a9c..942cd2a25 100644 --- a/provider/schema/set_nested_attribute_test.go +++ b/provider/schema/set_nested_attribute_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" @@ -19,7 +21,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSetNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -563,6 +564,34 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index 4aa669bf1..aded0988e 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -233,6 +233,12 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/single_nested_attribute_test.go b/provider/schema/single_nested_attribute_test.go index a471cbc20..6d40629a2 100644 --- a/provider/schema/single_nested_attribute_test.go +++ b/provider/schema/single_nested_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSingleNestedAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -524,6 +525,34 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectValidators(t *testing.T) { t.Parallel() diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index 7ab7a0c42..eda7a02c4 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -4,13 +4,14 @@ package schema import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Ensure the implementation satisifies the desired interfaces. @@ -174,6 +175,12 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns false as write-only attributes are not relevant to provider schemas, +// as these schemas describe data explicitly not saved to any artifact. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/provider/schema/string_attribute_test.go b/provider/schema/string_attribute_test.go index 4d24ef73d..58b0cafeb 100644 --- a/provider/schema/string_attribute_test.go +++ b/provider/schema/string_attribute_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" @@ -16,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { @@ -384,6 +385,34 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index abb0b8708..fa80f565c 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -152,6 +152,16 @@ type BoolAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Bool + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -229,6 +239,11 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a BoolAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/bool_attribute_test.go b/resource/schema/bool_attribute_test.go index d4b85ef2d..e2d77dd42 100644 --- a/resource/schema/bool_attribute_test.go +++ b/resource/schema/bool_attribute_test.go @@ -512,6 +512,40 @@ func TestBoolAttributeIsSensitive(t *testing.T) { } } +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.BoolAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestBoolAttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index 7b97625d9..e06600ab4 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -153,6 +153,16 @@ type DynamicAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Dynamic + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -215,6 +225,11 @@ func (a DynamicAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a DynamicAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // DynamicDefaultValue returns the Default field value. func (a DynamicAttribute) DynamicDefaultValue() defaults.Dynamic { return a.Default diff --git a/resource/schema/dynamic_attribute_test.go b/resource/schema/dynamic_attribute_test.go index f99dc598c..93c5010f3 100644 --- a/resource/schema/dynamic_attribute_test.go +++ b/resource/schema/dynamic_attribute_test.go @@ -397,6 +397,40 @@ func TestDynamicAttributeIsSensitive(t *testing.T) { } } +func TestDynamicAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.DynamicAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestDynamicAttributeDynamicDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index 9e8e7a22a..3064b4ed9 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -155,6 +155,16 @@ type Float32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float32 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,11 @@ func (a Float32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float32_attribute_test.go b/resource/schema/float32_attribute_test.go index 75f1a9637..3c90d0081 100644 --- a/resource/schema/float32_attribute_test.go +++ b/resource/schema/float32_attribute_test.go @@ -512,6 +512,40 @@ func TestFloat32AttributeIsSensitive(t *testing.T) { } } +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat32AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 7d762a4a2..205af3f98 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -155,6 +155,16 @@ type Float64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Float64 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,11 @@ func (a Float64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Float64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float64_attribute_test.go b/resource/schema/float64_attribute_test.go index a7d155c9f..939080080 100644 --- a/resource/schema/float64_attribute_test.go +++ b/resource/schema/float64_attribute_test.go @@ -512,6 +512,40 @@ func TestFloat64AttributeIsSensitive(t *testing.T) { } } +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Float64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestFloat64AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index 41b74bcf3..d3f97d60b 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -155,6 +155,16 @@ type Int32Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int32 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,11 @@ func (a Int32Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int32Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int32_attribute_test.go b/resource/schema/int32_attribute_test.go index 152d957f7..48eb3d2f2 100644 --- a/resource/schema/int32_attribute_test.go +++ b/resource/schema/int32_attribute_test.go @@ -512,6 +512,40 @@ func TestInt32AttributeIsSensitive(t *testing.T) { } } +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int32Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt32AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index 65ec795e9..c65eb41fa 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -155,6 +155,16 @@ type Int64Attribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Int64 + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -232,6 +242,11 @@ func (a Int64Attribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a Int64Attribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int64_attribute_test.go b/resource/schema/int64_attribute_test.go index a961c1fb8..032f523c9 100644 --- a/resource/schema/int64_attribute_test.go +++ b/resource/schema/int64_attribute_test.go @@ -512,6 +512,40 @@ func TestInt64AttributeIsSensitive(t *testing.T) { } } +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.Int64Attribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestInt64AttributeValidateImplementation(t *testing.T) { t.Parallel() diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 1dc0e0e8c..9c1536dbe 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -168,6 +168,16 @@ type ListAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a list @@ -232,6 +242,11 @@ func (a ListAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue returns the Default field value. func (a ListAttribute) ListDefaultValue() defaults.List { return a.Default diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go index 784ec70d6..07db0c333 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -403,6 +403,40 @@ func TestListAttributeIsSensitive(t *testing.T) { } } +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListAttributeListDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 95fd2ba01..ee1845bb5 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -178,6 +178,19 @@ type ListNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.List + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +273,11 @@ func (a ListNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ListNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ListDefaultValue returns the Default field value. func (a ListNestedAttribute) ListDefaultValue() defaults.List { return a.Default @@ -284,6 +302,14 @@ func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fws resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ListDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go index a73c7a843..290fd0dc7 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestListNestedAttributeIsSensitive(t *testing.T) { } } +func TestListNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestListNestedAttributeListDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index ac50f63f8..f76a295c3 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -171,6 +171,16 @@ type MapAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a map @@ -235,6 +245,11 @@ func (a MapAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue returns the Default field value. func (a MapAttribute) MapDefaultValue() defaults.Map { return a.Default diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go index 4f56036d5..9e48ea787 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -403,6 +403,40 @@ func TestMapAttributeIsSensitive(t *testing.T) { } } +func TestMapAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapAttributeMapDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index ab2230b3b..db868f726 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -178,6 +178,19 @@ type MapNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Map + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -260,6 +273,11 @@ func (a MapNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a MapNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // MapDefaultValue returns the Default field value. func (a MapNestedAttribute) MapDefaultValue() defaults.Map { return a.Default @@ -284,6 +302,14 @@ func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.MapDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go index 1fd1a7624..4c7a72489 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestMapNestedAttributeIsSensitive(t *testing.T) { } } +func TestMapNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestMapNestedAttributeMapNestedDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index d2b9c59af..8f367592e 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -156,6 +156,16 @@ type NumberAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Number + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -218,6 +228,11 @@ func (a NumberAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a NumberAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // NumberDefaultValue returns the Default field value. func (a NumberAttribute) NumberDefaultValue() defaults.Number { return a.Default diff --git a/resource/schema/number_attribute_test.go b/resource/schema/number_attribute_test.go index 2bc8731ca..e9ff78cc8 100644 --- a/resource/schema/number_attribute_test.go +++ b/resource/schema/number_attribute_test.go @@ -398,6 +398,40 @@ func TestNumberAttributeIsSensitive(t *testing.T) { } } +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.NumberAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestNumberAttributeNumberDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 7b9fe6a56..03f35aa00 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -170,6 +170,16 @@ type ObjectAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into an @@ -234,6 +244,11 @@ func (a ObjectAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a ObjectAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue returns the Default field value. func (a ObjectAttribute) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go index 071e2293c..8f1b6cc3c 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -409,6 +409,40 @@ func TestObjectAttributeIsSensitive(t *testing.T) { } } +func TestObjectAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.ObjectAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestObjectAttributeObjectDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index 7a54221bb..843f58fb3 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -166,6 +166,16 @@ type SetAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Set + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the result of stepping into a set @@ -230,6 +240,11 @@ func (a SetAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SetAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // SetDefaultValue returns the Default field value. func (a SetAttribute) SetDefaultValue() defaults.Set { return a.Default diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go index 98483f724..9f7c6d0ac 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -403,6 +403,40 @@ func TestSetAttributeIsSensitive(t *testing.T) { } } +func TestSetAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SetAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetAttributeSetDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index dee37b591..56c449307 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -173,6 +173,19 @@ type SetNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Set + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // If WriteOnly is true for a nested attribute, all of its child attributes + // must also set WriteOnly to true and no child attribute can be Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -255,6 +268,11 @@ func (a SetNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SetNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // SetDefaultValue returns the Default field value. func (a SetNestedAttribute) SetDefaultValue() defaults.Set { return a.Default @@ -279,6 +297,14 @@ func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwsc resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.SetDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go index d148db457..8ea3cb58c 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -574,6 +574,40 @@ func TestSetNestedAttributeIsSensitive(t *testing.T) { } } +func TestSetNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSetNestedAttributeSetDefaultValue(t *testing.T) { t.Parallel() @@ -909,6 +943,94 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 3dbda942a..12cab9800 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -167,6 +167,16 @@ type SingleNestedAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.Object + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep returns the Attributes field value if step @@ -271,6 +281,11 @@ func (a SingleNestedAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a SingleNestedAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // ObjectDefaultValue returns the Default field value. func (a SingleNestedAttribute) ObjectDefaultValue() defaults.Object { return a.Default @@ -295,6 +310,14 @@ func (a SingleNestedAttribute) ValidateImplementation(ctx context.Context, req f resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) } + if a.IsWriteOnly() && !fwschema.ContainsAllWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidWriteOnlyNestedAttributeDiag(req.Path)) + } + + if a.IsComputed() && fwschema.ContainsAnyWriteOnlyChildAttributes(a) { + resp.Diagnostics.Append(fwschema.InvalidComputedNestedAttributeWithWriteOnlyDiag(req.Path)) + } + if a.ObjectDefaultValue() != nil { if !a.IsComputed() { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go index 1f20b65b3..f1b666566 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -538,6 +538,40 @@ func TestSingleNestedAttributeIsSensitive(t *testing.T) { } } +func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestSingleNestedAttributeObjectDefaultValue(t *testing.T) { t.Parallel() @@ -819,6 +853,86 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }, }, }, + "writeOnly-with-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "writeOnly-without-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a WriteOnly nested attribute that contains a non-WriteOnly child attribute.\n\n"+ + "Every child attribute of a WriteOnly nested attribute must also have WriteOnly set to true.", + ), + }, + }, + }, + "computed-without-child-writeOnly-no-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "computed-with-child-writeOnly-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is a Computed nested attribute that contains a WriteOnly child attribute.\n\n"+ + "Every child attribute of a Computed nested attribute must have WriteOnly set to false.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 7e3b8a1c2..693327016 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -152,6 +152,16 @@ type StringAttribute struct { // computed and the value could be altered by other changes then a default // should be avoided and a plan modifier should be used instead. Default defaults.String + + // WriteOnly indicates that Terraform will not store this attribute value + // in the plan or state artifacts. + // If WriteOnly is true, either Optional or Required must also be true. + // WriteOnly cannot be set with Computed. + // + // This functionality is only supported in Terraform 1.11 and later. + // Practitioners that choose a value for this attribute with older + // versions of Terraform will receive an error. + WriteOnly bool } // ApplyTerraform5AttributePathStep always returns an error as it is not @@ -214,6 +224,11 @@ func (a StringAttribute) IsSensitive() bool { return a.Sensitive } +// IsWriteOnly returns the WriteOnly field value. +func (a StringAttribute) IsWriteOnly() bool { + return a.WriteOnly +} + // StringDefaultValue returns the Default field value. func (a StringAttribute) StringDefaultValue() defaults.String { return a.Default diff --git a/resource/schema/string_attribute_test.go b/resource/schema/string_attribute_test.go index 65bc15b10..20f5ccf36 100644 --- a/resource/schema/string_attribute_test.go +++ b/resource/schema/string_attribute_test.go @@ -397,6 +397,40 @@ func TestStringAttributeIsSensitive(t *testing.T) { } } +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.StringAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func TestStringAttributeStringDefaultValue(t *testing.T) { t.Parallel() diff --git a/resource/schema/write_only_nested_attribute_validation_test.go b/resource/schema/write_only_nested_attribute_validation_test.go new file mode 100644 index 000000000..acc2ca1de --- /dev/null +++ b/resource/schema/write_only_nested_attribute_validation_test.go @@ -0,0 +1,1281 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func TestContainsAllWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() + tests := map[string]struct { + nestedAttr metaschema.NestedAttribute + expected bool + }{ + "empty nested attribute returns true": { + nestedAttr: schema.ListNestedAttribute{}, + expected: true, + }, + "list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly nested child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + }, + expected: false, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := fwschema.ContainsAllWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestContainsAnyWriteOnlyChildAttributes(t *testing.T) { + t.Parallel() + tests := map[string]struct { + nestedAttr metaschema.NestedAttribute + expected bool + }{ + "empty nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{}, + expected: false, + }, + "list nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "list nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "list nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "set nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "set_nested_attribute": schema.SetNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "map nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "map nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "map_nested_attribute": schema.MapNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + + "single nested attribute with writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + }, + }, + expected: false, + }, + "single nested attribute with multiple writeOnly child attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + }, + }, + }, + expected: true, + }, + "single nested attribute with writeOnly child nested attributes returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with non-writeOnly child nested attribute returns false": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + expected: false, + }, + "single nested attribute with one non-writeOnly child nested attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + "list_nested_attribute": schema.ListNestedAttribute{ + WriteOnly: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + Computed: true, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: false, + Computed: true, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "single nested attribute with one non-writeOnly nested child attribute returns true": { + nestedAttr: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + WriteOnly: false, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + WriteOnly: false, + }, + "float32_attribute": schema.Float32Attribute{ + WriteOnly: true, + }, + }, + }, + }, + }, + expected: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := fwschema.ContainsAnyWriteOnlyChildAttributes(tt.nestedAttr); got != tt.expected { + t.Errorf("ContainsAllWriteOnlyChildAttributes() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/resource/validate_config.go b/resource/validate_config.go index 40e1213bf..f35c4aa3f 100644 --- a/resource/validate_config.go +++ b/resource/validate_config.go @@ -8,6 +8,17 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// ValidateConfigClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the +// ValidateResourceConfig RPC, such as forward-compatible Terraform behavior +// changes. +type ValidateConfigClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + WriteOnlyAttributesAllowed bool +} + // ValidateConfigRequest represents a request to validate the // configuration of a resource. An instance of this request struct is // supplied as an argument to the Resource ValidateConfig receiver method @@ -19,6 +30,11 @@ type ValidateConfigRequest struct { // interpolation or other functionality that would prevent Terraform // from knowing the value at request time. Config tfsdk.Config + + // ClientCapabilities defines optionally supported protocol features for + // the ValidateResourceConfig RPC, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateConfigClientCapabilities } // ValidateConfigResponse represents a response to a diff --git a/schema/validator/bool.go b/schema/validator/bool.go index 64115e713..26f6df1a4 100644 --- a/schema/validator/bool.go +++ b/schema/validator/bool.go @@ -35,6 +35,11 @@ type BoolRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Bool + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // BoolResponse is a response to a BoolRequest. diff --git a/schema/validator/client_capabilities.go b/schema/validator/client_capabilities.go new file mode 100644 index 000000000..8f0bbe9a0 --- /dev/null +++ b/schema/validator/client_capabilities.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validator + +// ValidateSchemaClientCapabilities allows Terraform to publish information +// regarding optionally supported protocol features for the schema validation +// RPCs, such as forward-compatible Terraform behavior changes. +type ValidateSchemaClientCapabilities struct { + // WriteOnlyAttributesAllowed indicates that the Terraform client + // initiating the request supports write-only attributes for managed + // resources. + // + // This client capability is only populated during managed resource schema + // validation. + WriteOnlyAttributesAllowed bool +} diff --git a/schema/validator/dynamic.go b/schema/validator/dynamic.go index b035175a1..e5d2cb58c 100644 --- a/schema/validator/dynamic.go +++ b/schema/validator/dynamic.go @@ -35,6 +35,11 @@ type DynamicRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Dynamic + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // DynamicResponse is a response to a DynamicRequest. diff --git a/schema/validator/float32.go b/schema/validator/float32.go index c1cd8421d..9f38507b2 100644 --- a/schema/validator/float32.go +++ b/schema/validator/float32.go @@ -35,6 +35,11 @@ type Float32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float32Response is a response to a Float32Request. diff --git a/schema/validator/float64.go b/schema/validator/float64.go index f09111ac7..7c788d8f3 100644 --- a/schema/validator/float64.go +++ b/schema/validator/float64.go @@ -35,6 +35,11 @@ type Float64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Float64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Float64Response is a response to a Float64Request. diff --git a/schema/validator/int32.go b/schema/validator/int32.go index 2cbbc3cc2..d13185226 100644 --- a/schema/validator/int32.go +++ b/schema/validator/int32.go @@ -35,6 +35,11 @@ type Int32Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int32 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int32Response is a response to a Int32Request. diff --git a/schema/validator/int64.go b/schema/validator/int64.go index 8e8accdcb..061ab1cf8 100644 --- a/schema/validator/int64.go +++ b/schema/validator/int64.go @@ -35,6 +35,11 @@ type Int64Request struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Int64 + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // Int64Response is a response to a Int64Request. diff --git a/schema/validator/list.go b/schema/validator/list.go index e5b6083d8..e2dc5ecd1 100644 --- a/schema/validator/list.go +++ b/schema/validator/list.go @@ -35,6 +35,11 @@ type ListRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.List + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ListResponse is a response to a ListRequest. diff --git a/schema/validator/map.go b/schema/validator/map.go index 2a41cc7ac..eda09a239 100644 --- a/schema/validator/map.go +++ b/schema/validator/map.go @@ -35,6 +35,11 @@ type MapRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Map + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // MapResponse is a response to a MapRequest. diff --git a/schema/validator/number.go b/schema/validator/number.go index ef7692c20..2bccf9ac6 100644 --- a/schema/validator/number.go +++ b/schema/validator/number.go @@ -35,6 +35,11 @@ type NumberRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Number + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // NumberResponse is a response to a NumberRequest. diff --git a/schema/validator/object.go b/schema/validator/object.go index 88029e0ad..a2d96a24c 100644 --- a/schema/validator/object.go +++ b/schema/validator/object.go @@ -35,6 +35,11 @@ type ObjectRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Object + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // ObjectResponse is a response to a ObjectRequest. diff --git a/schema/validator/set.go b/schema/validator/set.go index ce7cdea34..f3aaf0b0f 100644 --- a/schema/validator/set.go +++ b/schema/validator/set.go @@ -35,6 +35,11 @@ type SetRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.Set + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // SetResponse is a response to a SetRequest. diff --git a/schema/validator/string.go b/schema/validator/string.go index b453a7bfc..4427e2691 100644 --- a/schema/validator/string.go +++ b/schema/validator/string.go @@ -35,6 +35,11 @@ type StringRequest struct { // ConfigValue contains the value of the attribute for validation from the configuration. ConfigValue types.String + + // ClientCapabilities defines optionally supported protocol features for + // schema validation RPCs, such as forward-compatible Terraform + // behavior changes. + ClientCapabilities ValidateSchemaClientCapabilities } // StringResponse is a response to a StringRequest. diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 2297fad3c..b41e7901f 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -98,6 +98,10 @@ { "title": "Timeouts", "path": "resources/timeouts" + }, + { + "title": "Write-only Arguments", + "path": "resources/write-only-arguments" } ] }, diff --git a/website/docs/plugin/framework/handling-data/attributes/bool.mdx b/website/docs/plugin/framework/handling-data/attributes/bool.mdx index 576baff50..b1b7eb2e1 100644 --- a/website/docs/plugin/framework/handling-data/attributes/bool.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/bool.mdx @@ -105,6 +105,18 @@ The [`boolplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx index 44701cc24..66da060bf 100644 --- a/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/dynamic.mdx @@ -134,6 +134,18 @@ The [`dynamicplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float32.mdx b/website/docs/plugin/framework/handling-data/attributes/float32.mdx index 5f710dafe..f0a654718 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float32.mdx @@ -111,6 +111,18 @@ The [`float32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/float64.mdx b/website/docs/plugin/framework/handling-data/attributes/float64.mdx index da31351b6..a43e8b7ab 100644 --- a/website/docs/plugin/framework/handling-data/attributes/float64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/float64.mdx @@ -111,6 +111,18 @@ The [`float64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-pl Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/int32.mdx b/website/docs/plugin/framework/handling-data/attributes/int32.mdx index f0c4ad56c..cd9c443e9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int32.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int32.mdx @@ -111,6 +111,18 @@ The [`int32planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/int64.mdx b/website/docs/plugin/framework/handling-data/attributes/int64.mdx index 416976e45..6bba8efe4 100644 --- a/website/docs/plugin/framework/handling-data/attributes/int64.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/int64.mdx @@ -111,6 +111,18 @@ The [`int64planmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plug Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx index ea0272133..1a3bd86e6 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list-nested.mdx @@ -159,6 +159,20 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/list.mdx b/website/docs/plugin/framework/handling-data/attributes/list.mdx index 2104e2475..50ffcf6b5 100644 --- a/website/docs/plugin/framework/handling-data/attributes/list.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/list.mdx @@ -128,6 +128,18 @@ The [`listplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugi Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx index 210f25a9d..53424755a 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map-nested.mdx @@ -159,6 +159,20 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/map.mdx b/website/docs/plugin/framework/handling-data/attributes/map.mdx index 7eb65aa89..a5b3faea9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/map.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/map.mdx @@ -131,6 +131,18 @@ The [`mapplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/number.mdx b/website/docs/plugin/framework/handling-data/attributes/number.mdx index 4fb48a1ba..270a35070 100644 --- a/website/docs/plugin/framework/handling-data/attributes/number.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/number.mdx @@ -111,6 +111,18 @@ The [`numberplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/object.mdx b/website/docs/plugin/framework/handling-data/attributes/object.mdx index d7f2c837d..de7b56083 100644 --- a/website/docs/plugin/framework/handling-data/attributes/object.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/object.mdx @@ -165,6 +165,18 @@ Only the object attribute itself, not individual sub-attributes, can define its Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx index 01ce7e51f..9e4f8afe9 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set-nested.mdx @@ -159,6 +159,20 @@ The [`setplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/set.mdx b/website/docs/plugin/framework/handling-data/attributes/set.mdx index 512489aad..9f3d7dcd3 100644 --- a/website/docs/plugin/framework/handling-data/attributes/set.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/set.mdx @@ -128,6 +128,18 @@ The [`setplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx index 56675912f..24e52a883 100644 --- a/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/single-nested.mdx @@ -155,6 +155,20 @@ The [`objectplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + +If a nested attribute has the `WriteOnly` field set, all child attributes must also have `WriteOnly` set. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/handling-data/attributes/string.mdx b/website/docs/plugin/framework/handling-data/attributes/string.mdx index d423109dd..6d414dbff 100644 --- a/website/docs/plugin/framework/handling-data/attributes/string.mdx +++ b/website/docs/plugin/framework/handling-data/attributes/string.mdx @@ -112,6 +112,18 @@ The [`stringplanmodifier`](https://pkg.go.dev/github.com/hashicorp/terraform-plu Set the `Sensitive` field if the attribute value should always be considered [sensitive data](/terraform/language/state/sensitive-data). In Terraform, this will generally mask the value in practitioner output. This setting cannot be conditionally set and does not impact how data is stored in the state. +### WriteOnly + + + + Only managed resources implement this concept. + + + +Set the `WriteOnly` field to define a [write-only argument](/terraform/plugin/framework/resources/write-only-arguments). +Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) +and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. + ### Validation Set the `Validators` field to define [validation](/terraform/plugin/framework/validation#attribute-validation). This validation logic is ran in addition to any validation contained within a [custom type](#custom-types). diff --git a/website/docs/plugin/framework/resources/index.mdx b/website/docs/plugin/framework/resources/index.mdx index 61d8b5798..6aa9000c8 100644 --- a/website/docs/plugin/framework/resources/index.mdx +++ b/website/docs/plugin/framework/resources/index.mdx @@ -30,6 +30,7 @@ Further documentation is available for deeper resource concepts: - [Upgrade state](/terraform/plugin/framework/resources/state-upgrade) to transparently update state data outside plans. - [Validate](/terraform/plugin/framework/resources/validate-configuration) practitioner configuration against acceptable values. - [Timeouts](/terraform/plugin/framework/resources/timeouts) in practitioner configuration for use in resource create, read, update and delete functions. +- [Write-only Arguments](/terraform/plugin/framework/resources/write-only-arguments) are special types of attributes that can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not persisted in the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. ## Define Resource Type diff --git a/website/docs/plugin/framework/resources/write-only-arguments.mdx b/website/docs/plugin/framework/resources/write-only-arguments.mdx new file mode 100644 index 000000000..b418e34f0 --- /dev/null +++ b/website/docs/plugin/framework/resources/write-only-arguments.mdx @@ -0,0 +1,112 @@ +--- +page_title: 'Plugin Development - Framework: Write-only Arguments' +description: >- + How to implement write-only arguments with the provider development framework. +--- + +# Write-only Arguments + +Write-only arguments are managed resource attributes that are configured by practitioners but are not persisted to the Terraform plan or state artifacts. Write-only arguments are supported in Terraform 1.11 and later. +Write-only arguments should be used to handle secret values that do not need to be persisted in Terraform state, such as passwords, API keys, etc. +The provider is expected to be the terminal point for an ephemeral value, +which should either use the value by making the appropriate change to the API or ignore the value. Write-only arguments can accept [ephemeral values](/terraform/language/resources/ephemeral) and are not required to be consistent between plan and apply operations. + +## General Concepts + +The following are high level differences between `Required`/`Optional` arguments and write-only arguments: + +- Write-only arguments can accept ephemeral and non-ephemeral values + +- Write-only argument values are only available in the configuration. The prior state, planned state, and final state values for +write-only arguments should always be `null`. + - Provider developers do not need to explicitly set write-only argument values to `null` after using them as the plugin framework will handle the nullification of write-only arguments for all RPCs. + +- Any value that is set for a write-only argument in the state or plan (during [Plan Modification](/terraform/plugin/framework/resources/plan-modification)) by the provider will be reverted to `null` by plugin framework before the RPC response is sent to Terraform. + +- Write-only argument values cannot produce a Terraform plan difference. + - This is because the prior state value for a write-only argument will always be `null` and the planned/final state value will also be `null`, therefore, it cannot produce a diff on its own. + - The one exception to this case is if the write-only argument is added to `requires_replace` during Plan Modification (i.e., using the [`RequiresReplace()`](/terraform/plugin/framework/resources/plan-modification#requiresreplace) plan modifier), in that case, the write-only argument will always cause a diff/trigger a resource recreation + +- Since write-only arguments can accept ephemeral values, write-only argument configuration values are not expected to be consistent between plan and apply. + +## Schema + +An attribute can be made write-only by setting the `WriteOnly` field to `true` in the schema. Attributes with `WriteOnly` set to `true` must also have +one of `Required` or `Optional` set to `true`. If a nested attribute has `WriteOnly` set to `true`, all child attributes must also have `WriteOnly` set to `true`. +`Computed` cannot be set to true for write-only arguments. + +**Schema example:** + +```go +"password_wo": schema.StringAttribute{ + Required: true, + WriteOnly: true, +}, +``` + +## Retrieving Write-only Values + +Write-only argument values should be retrieved from the configuration instead of the plan. Refer to [accessing values](/terraform/plugin/framework/handling-data/accessing-values) for more details on +retrieving values from configuration. + +## PreferWriteOnlyAttribute Validators + +The `PreferWriteOnlyAttribute()` validators available in the [`terraform-plugin-framework-validators` Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) +can be used when you have a write-only version of an existing attribute, and you want to encourage practitioners to use the write-only version whenever possible. + +The validator returns a warning if the Terraform client is 1.11 or above and the value of the existing attribute is non-null. + +`PreferWriteOnlyAttribute()` is available as a resource-level validator in the [`resourcevalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator) or +as an attribute-level validator in the `[type]validator` packages (i.e., [`stringvalidator` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator)) + +Usage: + +```go +// Resource-level validator +// Used inside a resource.Resource type ConfigValidators method + _ = []resource.ConfigValidator{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value + resourcevalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password"), + path.MatchRoot("password_wo"), + ), + } + +// Attribute-level validator +// Used within a Schema method of a Resource + _ = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "password": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + // Throws a warning diagnostic encouraging practitioners to use + // password_wo if password has a known value. + stringvalidator.PreferWriteOnlyAttribute( + path.MatchRoot("password_wo"), + ), + }, + }, + "password_wo": schema.StringAttribute{ + WriteOnly: true, + Optional: true, + }, + }, + } +``` + +```hcl +resource "example_db_instance" "ex" { + username = "foo" + password = "bar" # returns a warning encouraging practitioners to use `password_wo` instead. +} +``` + +## Best Practices + +Since write-only arguments have no prior values, user intent or value changes cannot be determined with a write-only argument alone. To determine when to use/not use a write-only argument value in your provider, we recommend one of the following: + +- Pair write-only arguments with a configuration attribute (required or optional) to “trigger” the use of the write-only argument + - For example, a `password_wo` write-only argument can be paired with a configured `password_wo_version` attribute. When the `password_wo_version` is modified, the provider will send the `password_wo` value to the API. +- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value. +- Use the resource's [private state] to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs. \ No newline at end of file