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..d033e8175 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -251,7 +251,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..8d80c31b6 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" @@ -606,6 +607,57 @@ func TestListAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.ListAttribute{ + Computed: true, + Default: testdefaults.List{ + DefaultListMethod: func(ctx context.Context, req defaults.ListRequest, resp *defaults.ListResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..fb18c62f3 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -275,7 +275,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..9a9928d7c 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -817,6 +818,69 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: testdefaults.List{ + DefaultListMethod: func(ctx context.Context, req defaults.ListRequest, resp *defaults.ListResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..8268b57c9 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -254,7 +254,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..5c41feaf9 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" @@ -605,6 +606,57 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.MapAttribute{ + Computed: true, + Default: testdefaults.Map{ + DefaultMapMethod: func(ctx context.Context, req defaults.MapRequest, resp *defaults.MapResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..daebc4359 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -275,7 +275,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..0a22b5bfc 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -817,6 +818,69 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: testdefaults.Map{ + DefaultMapMethod: func(ctx context.Context, req defaults.MapRequest, resp *defaults.MapResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..93cddfe9a 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -253,7 +253,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..e9a1123d9 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/path" @@ -659,6 +660,63 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "test_attr": types.StringType, + }, + Computed: true, + Default: testdefaults.Object{ + DefaultObjectMethod: func(ctx context.Context, req defaults.ObjectRequest, resp *defaults.ObjectResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..80d551131 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -249,7 +249,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..5127c88b0 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -594,6 +595,57 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.SetAttribute{ + Computed: true, + Default: testdefaults.Set{ + DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..191991604 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -270,7 +270,30 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..6eae45b3f 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -817,6 +818,69 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + Default: testdefaults.Set{ + DefaultSetMethod: func(ctx context.Context, req defaults.SetRequest, resp *defaults.SetResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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..a47ef6541 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -294,4 +294,31 @@ 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 defaultResp.Diagnostics.HasError() { + return + } + + 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..ba9fb1309 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testdefaults" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -757,6 +758,66 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, + "default-with-error-diagnostic": { + attribute: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "test_attr": schema.StringAttribute{ + Computed: true, + }, + }, + Computed: true, + Default: testdefaults.Object{ + DefaultObjectMethod: func(ctx context.Context, req defaults.ObjectRequest, resp *defaults.ObjectResponse) { + resp.Diagnostics.AddError("error summary", "error detail") + }, + }, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + // Only the Default error should be returned, not type validation errors. + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + }, + "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 {