From 73e0889d0ef77c41cde34498ad5c99436f164249 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 28 Feb 2024 10:34:21 -0500 Subject: [PATCH] resource/schema: Ensure invalid attribute default value errors are raised Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/590 Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 Previously the logic handling attribute `Default` values would silently ignore any type errors, which would lead to confusing planning data behaviors. This updates the logic to raise those error properly and adds covering unit testing. These error messages are using the underlying `tftypes` type system errors which is currently a pragmatic compromise throughout various parts of the framework logic that bridges between both type systems to save additional type assertion logic and potential bugs relating to those conversions. In the future if the internal `tftypes` handling and exported fields are replaced with the framework type system types, this logic would instead return error messaging based on the framework type system errors. This also will enhance the schema validation logic to check any `Default` response value and compare its type to the schema, which will raise framework type system errors during the `GetProviderSchema` RPC, or during schema unit testing if provider developers have implemented that additional testing. --- .../unreleased/BUG FIXES-20240228-103338.yaml | 5 + internal/fwschema/diagnostics.go | 21 + internal/fwschemadata/data_default.go | 14 +- internal/fwschemadata/data_default_test.go | 1347 +++++++++++++++-- resource/schema/list_attribute.go | 23 +- resource/schema/list_attribute_test.go | 30 + resource/schema/list_nested_attribute.go | 23 +- resource/schema/list_nested_attribute_test.go | 36 + resource/schema/map_attribute.go | 23 +- resource/schema/map_attribute_test.go | 30 + resource/schema/map_nested_attribute.go | 23 +- resource/schema/map_nested_attribute_test.go | 36 + resource/schema/object_attribute.go | 23 +- resource/schema/object_attribute_test.go | 34 + resource/schema/set_attribute.go | 23 +- resource/schema/set_attribute_test.go | 30 + resource/schema/set_nested_attribute.go | 23 +- resource/schema/set_nested_attribute_test.go | 36 + resource/schema/single_nested_attribute.go | 23 + .../schema/single_nested_attribute_test.go | 35 + 20 files changed, 1655 insertions(+), 183 deletions(-) create mode 100644 .changes/unreleased/BUG FIXES-20240228-103338.yaml diff --git a/.changes/unreleased/BUG FIXES-20240228-103338.yaml b/.changes/unreleased/BUG FIXES-20240228-103338.yaml new file mode 100644 index 000000000..9bbdc6947 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20240228-103338.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'resource/schema: Ensured invalid attribute default value errors are raised' +time: 2024-02-28T10:33:38.517635-05:00 +custom: + Issue: "930" diff --git a/internal/fwschema/diagnostics.go b/internal/fwschema/diagnostics.go index 69429b1a9..c79531326 100644 --- a/internal/fwschema/diagnostics.go +++ b/internal/fwschema/diagnostics.go @@ -6,6 +6,7 @@ package fwschema import ( "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" ) @@ -43,3 +44,23 @@ func AttributeMissingElementTypeDiag(attributePath path.Path) diag.Diagnostic { "One of these fields is required to prevent other unexpected errors or panics.", ) } + +func AttributeDefaultElementTypeMismatchDiag(attributePath path.Path, expectedElementType attr.Type, actualElementType attr.Type) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Attribute 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 has a default value of element type %q, but the schema expects a type of %q. ", attributePath, actualElementType, expectedElementType)+ + "The default value must match the type of the schema.", + ) +} + +func AttributeDefaultTypeMismatchDiag(attributePath path.Path, expectedType attr.Type, actualType attr.Type) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Attribute 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 has a default value of type %q, but the schema expects a type of %q. ", attributePath, actualType, expectedType)+ + "The default value must match the type of the schema.", + ) +} diff --git a/internal/fwschemadata/data_default.go b/internal/fwschemadata/data_default.go index a69845c48..75424bade 100644 --- a/internal/fwschemadata/data_default.go +++ b/internal/fwschemadata/data_default.go @@ -21,6 +21,7 @@ import ( // when configRaw contains a null value at the same path. func (d *Data) TransformDefaults(ctx context.Context, configRaw tftypes.Value) diag.Diagnostics { var diags diag.Diagnostics + var err error configData := Data{ Description: DataDescriptionConfiguration, @@ -28,8 +29,7 @@ func (d *Data) TransformDefaults(ctx context.Context, configRaw tftypes.Value) d TerraformValue: configRaw, } - // Errors are handled as richer diag.Diagnostics instead. - d.TerraformValue, _ = tftypes.Transform(d.TerraformValue, func(tfTypePath *tftypes.AttributePath, tfTypeValue tftypes.Value) (tftypes.Value, error) { + d.TerraformValue, err = tftypes.Transform(d.TerraformValue, func(tfTypePath *tftypes.AttributePath, tfTypeValue tftypes.Value) (tftypes.Value, error) { fwPath, fwPathDiags := fromtftypes.AttributePath(ctx, tfTypePath, d.Schema) diags.Append(fwPathDiags...) @@ -303,5 +303,15 @@ func (d *Data) TransformDefaults(ctx context.Context, configRaw tftypes.Value) d return tfTypeValue, nil }) + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + if err != nil { + diags.Append(diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: "+err.Error(), + )) + } + return diags } diff --git a/internal/fwschemadata/data_default_test.go b/internal/fwschemadata/data_default_test.go index 1bcdb5c3e..10f0b10d8 100644 --- a/internal/fwschemadata/data_default_test.go +++ b/internal/fwschemadata/data_default_test.go @@ -1496,6 +1496,104 @@ func TestDataDefault(t *testing.T) { ), }, }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "list-attribute-null-invalid-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "list_attribute": testschema.AttributeWithListDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally incorrect element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_attribute": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "list_attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_attribute": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "list_attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, nil, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "list_attribute": testschema.AttributeWithListDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally incorrect element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_attribute": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "list_attribute": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"list_attribute\"): can't use tftypes.List[tftypes.Bool] as tftypes.List[tftypes.String]", + ), + }, + }, "list-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, @@ -1961,6 +2059,104 @@ func TestDataDefault(t *testing.T) { ), }, }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "map-attribute-null-invalid-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "map_attribute": testschema.AttributeWithMapDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally incorrect element type + types.BoolType, + map[string]attr.Value{ + "b": types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_attribute": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "map_attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_attribute": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "map_attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, nil, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "map_attribute": testschema.AttributeWithMapDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally incorrect element type + types.BoolType, + map[string]attr.Value{ + "b": types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_attribute": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "map_attribute": tftypes.NewValue(tftypes.Map{ + ElementType: tftypes.String, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"map_attribute\"): can't use tftypes.Map[tftypes.Bool] as tftypes.Map[tftypes.String]", + ), + }, + }, "map-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, @@ -2407,6 +2603,13 @@ func TestDataDefault(t *testing.T) { fmt.Sprintf("expected %s, got: %s", path.Root("object_attribute"), req.Path), ) } + + // Response value type must conform to the schema or an error will be returned. + resp.PlanValue = types.ObjectNull( + map[string]attr.Type{ + "test_attribute": types.StringType, + }, + ) }, }, }, @@ -2796,7 +2999,8 @@ func TestDataDefault(t *testing.T) { ), }, }, - "object-attribute-null-unmodified-default-nil": { + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "object-attribute-null-invalid-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: testschema.Schema{ @@ -2804,7 +3008,15 @@ func TestDataDefault(t *testing.T) { "object_attribute": testschema.AttributeWithObjectDefaultValue{ Optional: true, AttributeTypes: map[string]attr.Type{"a": types.StringType}, - Default: nil, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + // intentionally invalid attribute types + map[string]attr.Type{"invalid": types.BoolType}, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }, + ), + ), }, }, }, @@ -2847,7 +3059,15 @@ func TestDataDefault(t *testing.T) { "object_attribute": testschema.AttributeWithObjectDefaultValue{ Optional: true, AttributeTypes: map[string]attr.Type{"a": types.StringType}, - Default: nil, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + // intentionally invalid attribute types + map[string]attr.Type{"invalid": types.BoolType}, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }, + ), + ), }, }, }, @@ -2868,69 +3088,150 @@ func TestDataDefault(t *testing.T) { }, ), }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"object_attribute\"): can't use tftypes.Object[\"invalid\":tftypes.Bool] as tftypes.Object[\"a\":tftypes.String]", + ), + }, }, - "set-attribute-request-path": { + "object-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ - Description: fwschemadata.DataDescriptionPlan, + Description: fwschemadata.DataDescriptionState, Schema: testschema.Schema{ Attributes: map[string]fwschema.Attribute{ - "set_attribute": testschema.AttributeWithSetDefaultValue{ - Optional: true, - Computed: true, - ElementType: types.StringType, - Default: testdefaults.Set{ - DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { - if !req.Path.Equal(path.Root("set_attribute")) { - resp.Diagnostics.AddError( - "unexpected req.Path value", - fmt.Sprintf("expected %s, got: %s", path.Root("set_attribute"), req.Path), - ) - } - }, - }, + "object_attribute": testschema.AttributeWithObjectDefaultValue{ + Optional: true, + AttributeTypes: map[string]attr.Type{"a": types.StringType}, + Default: nil, }, }, }, TerraformValue: tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "set_attribute": tftypes.Set{ElementType: tftypes.String}, + "object_attribute": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, }, }, map[string]tftypes.Value{ - "set_attribute": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + "object_attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "one"), + }), }, ), }, - rawConfig: tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "set_attribute": tftypes.Set{ElementType: tftypes.String}, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object_attribute": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, + }, }, - }, map[string]tftypes.Value{ - "set_attribute": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + "object_attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, nil, + ), }, ), expected: &fwschemadata.Data{ - Description: fwschemadata.DataDescriptionPlan, + Description: fwschemadata.DataDescriptionState, Schema: testschema.Schema{ Attributes: map[string]fwschema.Attribute{ - "set_attribute": testschema.AttributeWithSetDefaultValue{ - Optional: true, - Computed: true, - ElementType: types.StringType, - Default: testdefaults.Set{ - DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { - if !req.Path.Equal(path.Root("set_attribute")) { - resp.Diagnostics.AddError( - "unexpected req.Path value", - fmt.Sprintf("expected %s, got: %s", path.Root("set_attribute"), req.Path), - ) - } - }, - }, - }, - }, + "object_attribute": testschema.AttributeWithObjectDefaultValue{ + Optional: true, + AttributeTypes: map[string]attr.Type{"a": types.StringType}, + Default: nil, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "object_attribute": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, + }, + }, + map[string]tftypes.Value{ + "object_attribute": tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{"a": tftypes.String}, + }, map[string]tftypes.Value{ + "a": tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + }, + "set-attribute-request-path": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "set_attribute": testschema.AttributeWithSetDefaultValue{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Default: testdefaults.Set{ + DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { + if !req.Path.Equal(path.Root("set_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("set_attribute"), req.Path), + ) + } + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_attribute": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "set_attribute": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + }, + ), + }, + rawConfig: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_attribute": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "set_attribute": tftypes.NewValue(tftypes.Set{ElementType: tftypes.String}, nil), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionPlan, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "set_attribute": testschema.AttributeWithSetDefaultValue{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Default: testdefaults.Set{ + DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { + if !req.Path.Equal(path.Root("set_attribute")) { + resp.Diagnostics.AddError( + "unexpected req.Path value", + fmt.Sprintf("expected %s, got: %s", path.Root("set_attribute"), req.Path), + ) + } + }, + }, + }, + }, }, TerraformValue: tftypes.NewValue( tftypes.Object{ @@ -3261,6 +3562,104 @@ func TestDataDefault(t *testing.T) { ), }, }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "set-attribute-null-invalid-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "set_attribute": testschema.AttributeWithSetDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: setdefault.StaticValue( + types.SetValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_attribute": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "set_attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_attribute": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "set_attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, nil, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "set_attribute": testschema.AttributeWithSetDefaultValue{ + Optional: true, + ElementType: types.StringType, + Default: setdefault.StaticValue( + types.SetValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_attribute": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "set_attribute": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.String, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.String, "one"), + }), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"set_attribute\"): can't use tftypes.Set[tftypes.Bool] as tftypes.Set[tftypes.String]", + ), + }, + }, "set-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, @@ -4170,7 +4569,8 @@ func TestDataDefault(t *testing.T) { ), }, }, - "list-nested-attribute-null-unmodified-default-nil": { + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "list-nested-attribute-null-invalid-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ @@ -4185,7 +4585,15 @@ func TestDataDefault(t *testing.T) { }, }, }, - Default: nil, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally invalid element type + types.StringType, + []attr.Value{ + types.StringValue("invalid"), + }, + ), + ), }, }, }, @@ -4265,7 +4673,15 @@ func TestDataDefault(t *testing.T) { }, }, }, - Default: nil, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally invalid element type + types.StringType, + []attr.Value{ + types.StringValue("invalid"), + }, + ), + ), }, }, }, @@ -4306,21 +4722,31 @@ func TestDataDefault(t *testing.T) { }, ), }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"list_nested\"): can't use tftypes.List[tftypes.String] as tftypes.List[tftypes.Object[\"string_attribute\":tftypes.String]]", + ), + }, }, - "list-nested-attribute-string-attribute-not-null-unmodified-default": { + "list-nested-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "list_nested": schema.ListNestedAttribute{ + "list_nested": testschema.NestedAttributeWithListDefaultValue{ + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.AttributeWithStringDefaultValue{ + "string_attribute": testschema.Attribute{ Computed: true, - Default: stringdefault.StaticString("two"), + Type: types.StringType, }, }, }, + Default: nil, }, }, }, @@ -4382,18 +4808,7 @@ func TestDataDefault(t *testing.T) { }, }, }, - []tftypes.Value{ - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "one"), - }, - ), - }, + nil, ), }, ), @@ -4401,15 +4816,17 @@ func TestDataDefault(t *testing.T) { Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "list_nested": schema.ListNestedAttribute{ + "list_nested": testschema.NestedAttributeWithListDefaultValue{ + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.AttributeWithStringDefaultValue{ + "string_attribute": testschema.Attribute{ Computed: true, - Default: stringdefault.StaticString("two"), + Type: types.StringType, }, }, }, + Default: nil, }, }, }, @@ -4451,7 +4868,7 @@ func TestDataDefault(t *testing.T) { ), }, }, - "list-nested-attribute-string-attribute-null-unmodified-no-default": { + "list-nested-attribute-string-attribute-not-null-unmodified-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ @@ -4459,8 +4876,9 @@ func TestDataDefault(t *testing.T) { "list_nested": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ + "string_attribute": testschema.AttributeWithStringDefaultValue{ Computed: true, + Default: stringdefault.StaticString("two"), }, }, }, @@ -4533,7 +4951,7 @@ func TestDataDefault(t *testing.T) { }, }, map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, nil), + "string_attribute": tftypes.NewValue(tftypes.String, "one"), }, ), }, @@ -4547,8 +4965,9 @@ func TestDataDefault(t *testing.T) { "list_nested": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ + "string_attribute": testschema.AttributeWithStringDefaultValue{ Computed: true, + Default: stringdefault.StaticString("two"), }, }, }, @@ -4593,7 +5012,7 @@ func TestDataDefault(t *testing.T) { ), }, }, - "list-nested-attribute-string-attribute-null-modified-default": { + "list-nested-attribute-string-attribute-null-unmodified-no-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ @@ -4601,9 +5020,8 @@ func TestDataDefault(t *testing.T) { "list_nested": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.AttributeWithStringDefaultValue{ + "string_attribute": schema.StringAttribute{ Computed: true, - Default: stringdefault.StaticString("two"), }, }, }, @@ -4690,9 +5108,8 @@ func TestDataDefault(t *testing.T) { "list_nested": schema.ListNestedAttribute{ NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.AttributeWithStringDefaultValue{ + "string_attribute": schema.StringAttribute{ Computed: true, - Default: stringdefault.StaticString("two"), }, }, }, @@ -4728,7 +5145,7 @@ func TestDataDefault(t *testing.T) { }, }, map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "two"), + "string_attribute": tftypes.NewValue(tftypes.String, "one"), }, ), }, @@ -4737,7 +5154,7 @@ func TestDataDefault(t *testing.T) { ), }, }, - "list-nested-attribute-string-attribute-null-unmodified-default-nil": { + "list-nested-attribute-string-attribute-null-modified-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ @@ -4747,7 +5164,7 @@ func TestDataDefault(t *testing.T) { Attributes: map[string]schema.Attribute{ "string_attribute": testschema.AttributeWithStringDefaultValue{ Computed: true, - Default: nil, + Default: stringdefault.StaticString("two"), }, }, }, @@ -4836,7 +5253,7 @@ func TestDataDefault(t *testing.T) { Attributes: map[string]schema.Attribute{ "string_attribute": testschema.AttributeWithStringDefaultValue{ Computed: true, - Default: nil, + Default: stringdefault.StaticString("two"), }, }, }, @@ -4872,7 +5289,7 @@ func TestDataDefault(t *testing.T) { }, }, map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "one"), + "string_attribute": tftypes.NewValue(tftypes.String, "two"), }, ), }, @@ -4881,13 +5298,157 @@ func TestDataDefault(t *testing.T) { ), }, }, - "map-nested-attribute-not-null-unmodified-default": { + "list-nested-attribute-string-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "map_nested": testschema.NestedAttributeWithMapDefaultValue{ - Computed: true, + "list_nested": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.AttributeWithStringDefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_nested": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "list_nested": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_nested": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "list_nested": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.AttributeWithStringDefaultValue{ + Computed: true, + Default: nil, + }, + }, + }, + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list_nested": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "list_nested": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + }, + "map-nested-attribute-not-null-unmodified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "map_nested": testschema.NestedAttributeWithMapDefaultValue{ + Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "string_attribute": testschema.Attribute{ @@ -5363,6 +5924,168 @@ func TestDataDefault(t *testing.T) { ), }, }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "map-nested-attribute-null-invalid-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "map_nested": testschema.NestedAttributeWithMapDefaultValue{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.Attribute{ + Computed: true, + Type: types.StringType, + }, + }, + }, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally invalid element type + types.StringType, + map[string]attr.Value{ + "test-key": types.StringValue("invalid"), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_nested": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "map_nested": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test-key": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_nested": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "map_nested": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + nil, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "map_nested": testschema.NestedAttributeWithMapDefaultValue{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.Attribute{ + Computed: true, + Type: types.StringType, + }, + }, + }, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally invalid element type + types.StringType, + map[string]attr.Value{ + "test-key": types.StringValue("invalid"), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map_nested": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "map_nested": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "test-key": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"map_nested\"): can't use tftypes.Map[tftypes.String] as tftypes.Map[tftypes.Object[\"string_attribute\":tftypes.String]]", + ), + }, + }, "map-nested-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, @@ -6057,8 +6780,188 @@ func TestDataDefault(t *testing.T) { }, }, }, - map[string]tftypes.Value{ - "test-key": tftypes.NewValue( + map[string]tftypes.Value{ + "test-key": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + }, + "set-nested-attribute-not-null-unmodified-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "set_nested": testschema.NestedAttributeWithSetDefaultValue{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.Attribute{ + Computed: true, + Type: types.StringType, + }, + }, + }, + Default: setdefault.StaticValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attribute": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "string_attribute": types.StringType, + }, map[string]attr.Value{ + "string_attribute": types.StringValue("two"), + }), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_nested": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "set_nested": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_nested": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "set_nested": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "set_nested": testschema.NestedAttributeWithSetDefaultValue{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attribute": testschema.Attribute{ + Computed: true, + Type: types.StringType, + }, + }, + }, + Default: setdefault.StaticValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attribute": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "string_attribute": types.StringType, + }, map[string]attr.Value{ + "string_attribute": types.StringValue("two"), + }), + }, + ), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set_nested": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "set_nested": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "string_attribute": tftypes.String, @@ -6074,38 +6977,20 @@ func TestDataDefault(t *testing.T) { ), }, }, - "set-nested-attribute-not-null-unmodified-default": { + "set-nested-attribute-null-unmodified-no-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "set_nested": testschema.NestedAttributeWithSetDefaultValue{ + "set_nested": schema.SetNestedAttribute{ Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.Attribute{ + "string_attribute": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, }, }, - Default: setdefault.StaticValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attribute": types.StringType, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "string_attribute": types.StringType, - }, map[string]attr.Value{ - "string_attribute": types.StringValue("two"), - }), - }, - ), - ), }, }, }, @@ -6167,18 +7052,7 @@ func TestDataDefault(t *testing.T) { }, }, }, - []tftypes.Value{ - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "one"), - }, - ), - }, + nil, ), }, ), @@ -6186,33 +7060,15 @@ func TestDataDefault(t *testing.T) { Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "set_nested": testschema.NestedAttributeWithSetDefaultValue{ + "set_nested": schema.SetNestedAttribute{ Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": testschema.Attribute{ + "string_attribute": schema.StringAttribute{ Computed: true, - Type: types.StringType, }, }, }, - Default: setdefault.StaticValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attribute": types.StringType, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "string_attribute": types.StringType, - }, map[string]attr.Value{ - "string_attribute": types.StringValue("two"), - }), - }, - ), - ), }, }, }, @@ -6254,20 +7110,38 @@ func TestDataDefault(t *testing.T) { ), }, }, - "set-nested-attribute-null-unmodified-no-default": { + "set-nested-attribute-null-modified-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "set_nested": schema.SetNestedAttribute{ + "set_nested": testschema.NestedAttributeWithSetDefaultValue{ Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ + "string_attribute": testschema.Attribute{ Computed: true, + Type: types.StringType, }, }, }, + Default: setdefault.StaticValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attribute": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "string_attribute": types.StringType, + }, map[string]attr.Value{ + "string_attribute": types.StringValue("two"), + }), + }, + ), + ), }, }, }, @@ -6337,15 +7211,33 @@ func TestDataDefault(t *testing.T) { Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ Attributes: map[string]schema.Attribute{ - "set_nested": schema.SetNestedAttribute{ + "set_nested": testschema.NestedAttributeWithSetDefaultValue{ Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ - "string_attribute": schema.StringAttribute{ + "string_attribute": testschema.Attribute{ Computed: true, + Type: types.StringType, }, }, }, + Default: setdefault.StaticValue( + types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attribute": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "string_attribute": types.StringType, + }, map[string]attr.Value{ + "string_attribute": types.StringValue("two"), + }), + }, + ), + ), }, }, }, @@ -6378,7 +7270,7 @@ func TestDataDefault(t *testing.T) { }, }, map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "one"), + "string_attribute": tftypes.NewValue(tftypes.String, "two"), }, ), }, @@ -6387,7 +7279,8 @@ func TestDataDefault(t *testing.T) { ), }, }, - "set-nested-attribute-null-modified-default": { + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "set-nested-attribute-null-invalid-default": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, Schema: schema.Schema{ @@ -6404,18 +7297,10 @@ func TestDataDefault(t *testing.T) { }, Default: setdefault.StaticValue( types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attribute": types.StringType, - }, - }, + // intentionally invalid element type + types.StringType, []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "string_attribute": types.StringType, - }, map[string]attr.Value{ - "string_attribute": types.StringValue("two"), - }), + types.StringValue("invalid"), }, ), ), @@ -6500,18 +7385,10 @@ func TestDataDefault(t *testing.T) { }, Default: setdefault.StaticValue( types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attribute": types.StringType, - }, - }, + // intentionally invalid element type + types.StringType, []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "string_attribute": types.StringType, - }, map[string]attr.Value{ - "string_attribute": types.StringValue("two"), - }), + types.StringValue("invalid"), }, ), ), @@ -6547,7 +7424,7 @@ func TestDataDefault(t *testing.T) { }, }, map[string]tftypes.Value{ - "string_attribute": tftypes.NewValue(tftypes.String, "two"), + "string_attribute": tftypes.NewValue(tftypes.String, "one"), }, ), }, @@ -6555,6 +7432,14 @@ func TestDataDefault(t *testing.T) { }, ), }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"set_nested\"): can't use tftypes.Set[tftypes.String] as tftypes.Set[tftypes.Object[\"string_attribute\":tftypes.String]]", + ), + }, }, "set-nested-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ @@ -7602,6 +8487,134 @@ func TestDataDefault(t *testing.T) { ), }, }, + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + "single-nested-attribute-null-invalid-default": { + data: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested": testschema.NestedAttributeWithObjectDefaultValue{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + // intentionally invalid attribute types + map[string]attr.Type{ + "invalid": types.BoolType, + }, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "single_nested": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "single_nested": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + rawConfig: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "single_nested": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "single_nested": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + nil, + ), + }, + ), + expected: &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionState, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested": testschema.NestedAttributeWithObjectDefaultValue{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "string_attribute": schema.StringAttribute{ + Computed: true, + }, + }, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + // intentionally invalid attribute types + map[string]attr.Type{ + "invalid": types.BoolType, + }, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }), + ), + }, + }, + }, + TerraformValue: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "single_nested": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "single_nested": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string_attribute": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string_attribute": tftypes.NewValue(tftypes.String, "one"), + }, + ), + }, + ), + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Error Handling Schema Defaults", + "An unexpected error occurred while handling schema default values. "+ + "Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"single_nested\"): can't use tftypes.Object[\"invalid\":tftypes.Bool] as tftypes.Object[\"string_attribute\":tftypes.String]", + ), + }, + }, "single-nested-attribute-null-unmodified-default-nil": { data: &fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index d136e85c4..2f3d45c95 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -251,7 +251,26 @@ func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema. resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) } - if !a.IsComputed() && a.ListDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.ListDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.ListRequest{ + Path: req.Path, + } + defaultResp := &defaults.ListResponse{} + + a.ListDefaultValue().DefaultList(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.ElementType != nil && !a.ElementType.Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.ElementType, defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go index d4f55f824..7d5133316 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -606,6 +606,36 @@ func TestListAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.ListAttribute{ + Computed: true, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"basetypes.StringType\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, "elementtype": { attribute: schema.ListAttribute{ Computed: true, diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 9b0cae319..548b754e1 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -275,7 +275,26 @@ func (a ListNestedAttribute) ListValidators() []validator.List { // errors or panics. This logic runs during the GetProviderSchema RPC and // should never include false positives. func (a ListNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { - if !a.IsComputed() && a.ListDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.ListDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.ListRequest{ + Path: req.Path, + } + defaultResp := &defaults.ListResponse{} + + a.ListDefaultValue().DefaultList(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.CustomType == nil && a.NestedObject.CustomType == nil && !a.NestedObject.Type().Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.NestedObject.Type(), defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go index 2aa6baa8b..b38b935b1 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -817,6 +817,42 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: listdefault.StaticValue( + types.ListValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"types.ObjectType[\\\"test_attr\\\":basetypes.StringType]\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index e3b393370..dca87aeb5 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -254,7 +254,26 @@ func (a MapAttribute) ValidateImplementation(ctx context.Context, req fwschema.V resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) } - if !a.IsComputed() && a.MapDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.MapDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.MapRequest{ + Path: req.Path, + } + defaultResp := &defaults.MapResponse{} + + a.MapDefaultValue().DefaultMap(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.ElementType != nil && !a.ElementType.Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.ElementType, defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go index 33a113f5a..2dc782164 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -605,6 +605,36 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.MapAttribute{ + Computed: true, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally invalid element type + types.BoolType, + map[string]attr.Value{ + "testkey": types.BoolValue(true), + }, + ), + ), + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"basetypes.StringType\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, "elementtype": { attribute: schema.MapAttribute{ Computed: true, diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index a55198e3a..49ac53865 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -275,7 +275,26 @@ func (a MapNestedAttribute) MapValidators() []validator.Map { // errors or panics. This logic runs during the GetProviderSchema RPC and // should never include false positives. func (a MapNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { - if !a.IsComputed() && a.MapDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.MapDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.MapRequest{ + Path: req.Path, + } + defaultResp := &defaults.MapResponse{} + + a.MapDefaultValue().DefaultMap(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.CustomType == nil && a.NestedObject.CustomType == nil && !a.NestedObject.Type().Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.NestedObject.Type(), defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go index e07692f9a..def681bd7 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -817,6 +817,42 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: mapdefault.StaticValue( + types.MapValueMust( + // intentionally invalid element type + types.BoolType, + map[string]attr.Value{ + "testkey": types.BoolValue(true), + }, + ), + ), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"types.ObjectType[\\\"test_attr\\\":basetypes.StringType]\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 77ebb0514..13f419b67 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -253,7 +253,26 @@ func (a ObjectAttribute) ValidateImplementation(ctx context.Context, req fwschem resp.Diagnostics.Append(fwschema.AttributeMissingAttributeTypesDiag(req.Path)) } - if !a.IsComputed() && a.ObjectDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.ObjectDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.ObjectRequest{ + Path: req.Path, + } + defaultResp := &defaults.ObjectResponse{} + + a.ObjectDefaultValue().DefaultObject(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.AttributeTypes != nil && !a.GetType().Equal(defaultResp.PlanValue.Type(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultTypeMismatchDiag(req.Path, a.GetType(), defaultResp.PlanValue.Type(ctx))) + } } } diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go index 7d58bb3da..5abac1418 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -659,6 +659,40 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-attributetypes": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Computed: true, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + // intentionally invalid attribute types + map[string]attr.Type{ + "invalid": types.BoolType, + }, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }, + ), + ), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of type \"types.ObjectType[\\\"invalid\\\":basetypes.BoolType]\", but the schema expects a type of \"types.ObjectType[\\\"test_attr\\\":basetypes.StringType]\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index a5026363a..8caa24f9e 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -249,7 +249,26 @@ func (a SetAttribute) ValidateImplementation(ctx context.Context, req fwschema.V resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) } - if !a.IsComputed() && a.SetDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.SetDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.SetRequest{ + Path: req.Path, + } + defaultResp := &defaults.SetResponse{} + + a.SetDefaultValue().DefaultSet(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.ElementType != nil && !a.ElementType.Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.ElementType, defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go index 2c1d53ed7..d8ab699ac 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -594,6 +594,36 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.SetAttribute{ + Computed: true, + Default: setdefault.StaticValue( + types.SetValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"basetypes.StringType\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, "elementtype": { attribute: schema.SetAttribute{ Computed: true, diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index f79ed503c..56d0c8ed0 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -270,7 +270,26 @@ func (a SetNestedAttribute) SetValidators() []validator.Set { // errors or panics. This logic runs during the GetProviderSchema RPC and // should never include false positives. func (a SetNestedAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { - if !a.IsComputed() && a.SetDefaultValue() != nil { - resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + if a.SetDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.SetRequest{ + Path: req.Path, + } + defaultResp := &defaults.SetResponse{} + + a.SetDefaultValue().DefaultSet(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.CustomType == nil && a.NestedObject.CustomType == nil && !a.NestedObject.Type().Equal(defaultResp.PlanValue.ElementType(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultElementTypeMismatchDiag(req.Path, a.NestedObject.Type(), defaultResp.PlanValue.ElementType(ctx))) + } } } diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go index eeae0635d..91e52217d 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -817,6 +817,42 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-elementtype": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: setdefault.StaticValue( + types.SetValueMust( + // intentionally invalid element type + types.BoolType, + []attr.Value{ + types.BoolValue(true), + }, + ), + ), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of element type \"basetypes.BoolType\", but the schema expects a type of \"types.ObjectType[\\\"test_attr\\\":basetypes.StringType]\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 074e2f5e2..76701f38e 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -294,4 +294,27 @@ func (a SingleNestedAttribute) ValidateImplementation(ctx context.Context, req f if !a.IsComputed() && a.ObjectDefaultValue() != nil { resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) } + + if a.ObjectDefaultValue() != nil { + if !a.IsComputed() { + resp.Diagnostics.Append(nonComputedAttributeWithDefaultDiag(req.Path)) + } + + // Validate Default implementation. This is safe unless the framework + // ever allows more dynamic Default implementations at which the + // implementation would be required to be validated at runtime. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/930 + defaultReq := defaults.ObjectRequest{ + Path: req.Path, + } + defaultResp := &defaults.ObjectResponse{} + + a.ObjectDefaultValue().DefaultObject(ctx, defaultReq, defaultResp) + + resp.Diagnostics.Append(defaultResp.Diagnostics...) + + if a.CustomType == nil && !a.GetType().Equal(defaultResp.PlanValue.Type(ctx)) { + resp.Diagnostics.Append(fwschema.AttributeDefaultTypeMismatchDiag(req.Path, a.GetType(), defaultResp.PlanValue.Type(ctx))) + } + } } diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go index a5034e006..363a124fe 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -757,6 +757,41 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-invalid-attributetypes": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + Computed: true, + Default: objectdefault.StaticValue( + types.ObjectValueMust( + map[string]attr.Type{ + "invalid": types.BoolType, + }, + map[string]attr.Value{ + "invalid": types.BoolValue(true), + }, + ), + ), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute 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\" has a default value of type \"types.ObjectType[\\\"invalid\\\":basetypes.BoolType]\", but the schema expects a type of \"types.ObjectType[\\\"test_attr\\\":basetypes.StringType]\". "+ + "The default value must match the type of the schema.", + ), + }, + }, + }, } for name, testCase := range testCases {