From b13eb0b87c60509927f881c883f86f08c16ae5ad Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 1 Dec 2022 09:34:57 +0000 Subject: [PATCH] Adding Validate() function to provider, resource and data source schema and to provider metaschema to prevent the use of reserved names for top-level attributes and blocks and invalid names for attributes and blocks at any level of nesting (#136) --- .changelog/548.txt | 15 + datasource/schema/schema.go | 129 +++++- datasource/schema/schema_test.go | 392 +++++++++++++++++- internal/fwschema/schema.go | 3 +- internal/fwserver/server.go | 16 + .../fwserver/server_getproviderschema_test.go | 173 ++++++++ provider/metaschema/schema.go | 71 +++- provider/metaschema/schema_test.go | 166 +++++++- provider/schema/schema.go | 125 +++++- provider/schema/schema_test.go | 392 +++++++++++++++++- resource/schema/schema.go | 129 +++++- resource/schema/schema_test.go | 392 +++++++++++++++++- 12 files changed, 1994 insertions(+), 9 deletions(-) create mode 100644 .changelog/548.txt diff --git a/.changelog/548.txt b/.changelog/548.txt new file mode 100644 index 000000000..909b4505a --- /dev/null +++ b/.changelog/548.txt @@ -0,0 +1,15 @@ +```release-note:bug +provider: Add `Validate` function to `Schema` to prevent usage of reserved and invalid names for attributes and blocks +``` + +```release-note:bug +provider: Add `Validate` function to `MetaSchema` to prevent usage of reserved and invalid names for attributes and blocks +``` + +```release-note:bug +resource: Add `Validate` function to `Schema` to prevent usage of reserved and invalid names for attributes and blocks +``` + +```release-note:bug +datasource: Add `Validate` function to `Schema` to prevent usage of reserved and invalid names for attributes and blocks +``` diff --git a/datasource/schema/schema.go b/datasource/schema/schema.go index 9d8a877eb..d071c68eb 100644 --- a/datasource/schema/schema.go +++ b/datasource/schema/schema.go @@ -2,12 +2,15 @@ package schema import ( "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Schema must satify the fwschema.Schema interface. @@ -122,6 +125,130 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +func (s Schema) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Raise error diagnostics when data source configuration uses reserved + // field names for root-level attributes. + reservedFieldNames := map[string]struct{}{ + "connection": {}, + "count": {}, + "depends_on": {}, + "lifecycle": {}, + "provider": {}, + "provisioner": {}, + } + + attributes := s.GetAttributes() + + for k, v := range attributes { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateAttributeFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + blocks := s.GetBlocks() + + for k, v := range blocks { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateBlockFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + return diags +} + +// validFieldNameRegex is used to verify that name used for attributes and blocks +// comply with the defined regular expression. +var validFieldNameRegex = regexp.MustCompile("^[a-z0-9_]+$") + +// validateAttributeFieldName verifies that the name used for an attribute complies with the regular +// expression defined in validFieldNameRegex. +func validateAttributeFieldName(path path.Path, name string, attr fwschema.Attribute) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + if na, ok := attr.(fwschema.NestedAttribute); ok { + nestedObject := na.GetNestedObject() + + if nestedObject == nil { + return diags + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + } + + return diags +} + +// validateBlockFieldName verifies that the name used for a block complies with the regular +// expression defined in validFieldNameRegex. +func validateBlockFieldName(path path.Path, name string, b fwschema.Block) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + nestedObject := b.GetNestedObject() + + if nestedObject == nil { + return diags + } + + blocks := nestedObject.GetBlocks() + + for k, v := range blocks { + d := validateBlockFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + return diags +} + // schemaAttributes is a datasource to fwschema type conversion function. func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { result := make(map[string]fwschema.Attribute, len(attributes)) diff --git a/datasource/schema/schema_test.go b/datasource/schema/schema_test.go index a572a3851..3b68a90de 100644 --- a/datasource/schema/schema_test.go +++ b/datasource/schema/schema_test.go @@ -7,13 +7,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { @@ -995,3 +996,392 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { }) } } + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("depends_on"), + "Schema Using Reserved Field Name", + `"depends_on" is a reserved field name`, + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("connection"), + "Schema Using Reserved Field Name", + `"connection" is a reserved field name`, + ), + }, + }, + "single-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "single-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "list-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + }, + "list-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("depends_on"), + "Schema Using Reserved Field Name", + `"depends_on" is a reserved field name`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("connection"), + "Schema Using Reserved Field Name", + `"connection" is a reserved field name`, + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "^": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/internal/fwschema/schema.go b/internal/fwschema/schema.go index 3bfffeaa0..a177db485 100644 --- a/internal/fwschema/schema.go +++ b/internal/fwschema/schema.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Schema is the core interface required for data sources, providers, and diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index da661cf30..60e276df6 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -245,6 +245,12 @@ func (s *Server) DataSourceSchemas(ctx context.Context) (map[string]fwschema.Sch return s.dataSourceSchemas, s.dataSourceSchemasDiags } + s.dataSourceSchemasDiags.Append(schemaResp.Schema.Validate()...) + + if s.dataSourceSchemasDiags.HasError() { + return s.dataSourceSchemas, s.dataSourceSchemasDiags + } + s.dataSourceSchemas[dataSourceTypeName] = schemaResp.Schema case datasource.DataSourceWithGetSchema: logging.FrameworkDebug(ctx, "Calling provider defined DataSource GetSchema", map[string]interface{}{logging.KeyDataSourceType: dataSourceTypeName}) @@ -293,6 +299,8 @@ func (s *Server) ProviderSchema(ctx context.Context) (fwschema.Schema, diag.Diag s.providerSchema = schemaResp.Schema s.providerSchemaDiags = schemaResp.Diagnostics + + s.providerSchemaDiags.Append(schemaResp.Schema.Validate()...) case provider.ProviderWithGetSchema: logging.FrameworkDebug(ctx, "Calling provider defined Provider GetSchema") schema, diags := providerIface.GetSchema(ctx) //nolint:staticcheck // Required internal usage until removal @@ -340,6 +348,8 @@ func (s *Server) ProviderMetaSchema(ctx context.Context) (fwschema.Schema, diag. s.providerMetaSchema = resp.Schema s.providerMetaSchemaDiags = resp.Diagnostics + s.providerMetaSchemaDiags.Append(resp.Schema.Validate()...) + return s.providerMetaSchema, s.providerMetaSchemaDiags } @@ -470,6 +480,12 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem return s.resourceSchemas, s.resourceSchemasDiags } + s.resourceSchemasDiags.Append(schemaResp.Schema.Validate()...) + + if s.resourceSchemasDiags.HasError() { + return s.resourceSchemas, s.resourceSchemasDiags + } + s.resourceSchemas[resourceTypeName] = schemaResp.Schema case resource.ResourceWithGetSchema: logging.FrameworkDebug(ctx, "Calling provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName}) diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index cf4317927..32e1f0223 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -5,12 +5,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -105,6 +107,63 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "datasourceschemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "$": datasourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source1" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + SchemaMethod: func(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = datasourceschema.Schema{ + Attributes: map[string]datasourceschema.Attribute{ + "test2": datasourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + }, "datasourceschemas-duplicate-type-name": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -275,6 +334,34 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "provider-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = providerschema.Schema{ + Attributes: map[string]providerschema.Attribute{ + "$": providerschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + ServerCapabilities: &fwserver.ServerCapabilities{ + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + }, "providermeta": { server: &fwserver.Server{ Provider: &testprovider.ProviderWithMetaSchema{ @@ -307,6 +394,36 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "providermeta-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{}, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "$": metaschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + }, "resourceschemas": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -373,6 +490,62 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "resourceschemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "$": resourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test2": resourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + }, "resourceschemas-duplicate-type-name": { server: &fwserver.Server{ Provider: &testprovider.Provider{ diff --git a/provider/metaschema/schema.go b/provider/metaschema/schema.go index dde25bf3c..7c8524076 100644 --- a/provider/metaschema/schema.go +++ b/provider/metaschema/schema.go @@ -2,12 +2,15 @@ package metaschema import ( "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Schema must satify the fwschema.Schema interface. @@ -96,6 +99,72 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +func (s Schema) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Raise error diagnostics when data source configuration uses reserved + // field names for root-level attributes. + reservedFieldNames := map[string]struct{}{ + "alias": {}, + "version": {}, + } + + attributes := s.GetAttributes() + + for k, v := range attributes { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateAttributeFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + return diags +} + +// validFieldNameRegex is used to verify that name used for attributes and blocks +// comply with the defined regular expression. +var validFieldNameRegex = regexp.MustCompile("^[a-z0-9_]+$") + +// validateAttributeFieldName verifies that the name used for an attribute complies with the regular +// expression defined in validFieldNameRegex. +func validateAttributeFieldName(path path.Path, name string, attr fwschema.Attribute) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + if na, ok := attr.(fwschema.NestedAttribute); ok { + nestedObject := na.GetNestedObject() + + if nestedObject == nil { + return diags + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + } + + return diags +} + // schemaAttributes is a provider to fwschema type conversion function. func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { result := make(map[string]fwschema.Attribute, len(attributes)) diff --git a/provider/metaschema/schema_test.go b/provider/metaschema/schema_test.go index e5722f9bc..1a716c5e9 100644 --- a/provider/metaschema/schema_test.go +++ b/provider/metaschema/schema_test.go @@ -7,13 +7,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { @@ -806,3 +807,166 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { }) } } + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema metaschema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: metaschema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "alias": metaschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("alias"), + "Schema Using Reserved Field Name", + `"alias" is a reserved field name`, + ), + }, + }, + "single-nested-attribute-using-nested-reserved-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "single_nested_attribute": metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "alias": metaschema.BoolAttribute{}, + }, + }, + }, + }, + }, + "list-nested-attribute-using-nested-reserved-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "list_nested_attribute": metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "alias": metaschema.Int64Attribute{}, + }, + }, + }, + }, + }, + }, + "attribute-using-invalid-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "^": metaschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-nested-invalid-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "single_nested_attribute": metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "^": metaschema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-invalid-field-names": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "$": metaschema.SingleNestedAttribute{ + Attributes: map[string]metaschema.Attribute{ + "^": metaschema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-nested-invalid-field-name": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "list_nested_attribute": metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "^": metaschema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-invalid-field-names": { + schema: metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "$": metaschema.ListNestedAttribute{ + NestedObject: metaschema.NestedAttributeObject{ + Attributes: map[string]metaschema.Attribute{ + "^": metaschema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/provider/schema/schema.go b/provider/schema/schema.go index 406d1fb04..11b1135b4 100644 --- a/provider/schema/schema.go +++ b/provider/schema/schema.go @@ -2,12 +2,15 @@ package schema import ( "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Schema must satify the fwschema.Schema interface. @@ -120,6 +123,126 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +func (s Schema) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Raise error diagnostics when data source configuration uses reserved + // field names for root-level attributes. + reservedFieldNames := map[string]struct{}{ + "alias": {}, + "version": {}, + } + + attributes := s.GetAttributes() + + for k, v := range attributes { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateAttributeFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + blocks := s.GetBlocks() + + for k, v := range blocks { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateBlockFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + return diags +} + +// validFieldNameRegex is used to verify that name used for attributes and blocks +// comply with the defined regular expression. +var validFieldNameRegex = regexp.MustCompile("^[a-z0-9_]+$") + +// validateAttributeFieldName verifies that the name used for an attribute complies with the regular +// expression defined in validFieldNameRegex. +func validateAttributeFieldName(path path.Path, name string, attr fwschema.Attribute) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + if na, ok := attr.(fwschema.NestedAttribute); ok { + nestedObject := na.GetNestedObject() + + if nestedObject == nil { + return diags + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + } + + return diags +} + +// validateBlockFieldName verifies that the name used for a block complies with the regular +// expression defined in validFieldNameRegex. +func validateBlockFieldName(path path.Path, name string, b fwschema.Block) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + nestedObject := b.GetNestedObject() + + if nestedObject == nil { + return diags + } + + blocks := nestedObject.GetBlocks() + + for k, v := range blocks { + d := validateBlockFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + return diags +} + // schemaAttributes is a provider to fwschema type conversion function. func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { result := make(map[string]fwschema.Attribute, len(attributes)) diff --git a/provider/schema/schema_test.go b/provider/schema/schema_test.go index 6ed36ee88..6833de900 100644 --- a/provider/schema/schema_test.go +++ b/provider/schema/schema_test.go @@ -7,13 +7,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { @@ -995,3 +996,392 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { }) } } + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "alias": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("alias"), + "Schema Using Reserved Field Name", + `"alias" is a reserved field name`, + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "version": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("version"), + "Schema Using Reserved Field Name", + `"version" is a reserved field name`, + ), + }, + }, + "single-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "alias": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "single-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "version": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "list-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "alias": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + }, + "list-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "version": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "alias": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "version": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("alias"), + "Schema Using Reserved Field Name", + `"alias" is a reserved field name`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("version"), + "Schema Using Reserved Field Name", + `"version" is a reserved field name`, + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "^": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/resource/schema/schema.go b/resource/schema/schema.go index ce19d9c78..91813203d 100644 --- a/resource/schema/schema.go +++ b/resource/schema/schema.go @@ -2,12 +2,15 @@ package schema import ( "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) // Schema must satify the fwschema.Schema interface. @@ -132,6 +135,130 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +func (s Schema) Validate() diag.Diagnostics { + var diags diag.Diagnostics + + // Raise error diagnostics when data source configuration uses reserved + // field names for root-level attributes. + reservedFieldNames := map[string]struct{}{ + "connection": {}, + "count": {}, + "depends_on": {}, + "lifecycle": {}, + "provider": {}, + "provisioner": {}, + } + + attributes := s.GetAttributes() + + for k, v := range attributes { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateAttributeFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + blocks := s.GetBlocks() + + for k, v := range blocks { + if _, ok := reservedFieldNames[k]; ok { + diags.AddAttributeError( + path.Root(k), + "Schema Using Reserved Field Name", + fmt.Sprintf("%q is a reserved field name", k), + ) + } + + d := validateBlockFieldName(path.Root(k), k, v) + + diags.Append(d...) + } + + return diags +} + +// validFieldNameRegex is used to verify that name used for attributes and blocks +// comply with the defined regular expression. +var validFieldNameRegex = regexp.MustCompile("^[a-z0-9_]+$") + +// validateAttributeFieldName verifies that the name used for an attribute complies with the regular +// expression defined in validFieldNameRegex. +func validateAttributeFieldName(path path.Path, name string, attr fwschema.Attribute) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + if na, ok := attr.(fwschema.NestedAttribute); ok { + nestedObject := na.GetNestedObject() + + if nestedObject == nil { + return diags + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + } + + return diags +} + +// validateBlockFieldName verifies that the name used for a block complies with the regular +// expression defined in validFieldNameRegex. +func validateBlockFieldName(path path.Path, name string, b fwschema.Block) diag.Diagnostics { + var diags diag.Diagnostics + + if !validFieldNameRegex.MatchString(name) { + diags.AddAttributeError( + path, + "Invalid Schema Field Name", + fmt.Sprintf("Field name %q is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.", name), + ) + } + + nestedObject := b.GetNestedObject() + + if nestedObject == nil { + return diags + } + + blocks := nestedObject.GetBlocks() + + for k, v := range blocks { + d := validateBlockFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + attributes := nestedObject.GetAttributes() + + for k, v := range attributes { + d := validateAttributeFieldName(path.AtName(k), k, v) + + diags.Append(d...) + } + + return diags +} + // schemaAttributes is a resource to fwschema type conversion function. func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { result := make(map[string]fwschema.Attribute, len(attributes)) diff --git a/resource/schema/schema_test.go b/resource/schema/schema_test.go index 2383d6036..c0f6b4b05 100644 --- a/resource/schema/schema_test.go +++ b/resource/schema/schema_test.go @@ -7,13 +7,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { @@ -1004,3 +1005,392 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { }) } } + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("depends_on"), + "Schema Using Reserved Field Name", + `"depends_on" is a reserved field name`, + ), + }, + }, + "block-using-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("connection"), + "Schema Using Reserved Field Name", + `"connection" is a reserved field name`, + ), + }, + }, + "single-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "single-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + "list-nested-attribute-using-nested-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + }, + "list-nested-block-using-nested-reserved-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "connection": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + "attribute-and-blocks-using-reserved-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + Blocks: map[string]schema.Block{ + "connection": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("depends_on"), + "Schema Using Reserved Field Name", + `"depends_on" is a reserved field name`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("connection"), + "Schema Using Reserved Field Name", + `"connection" is a reserved field name`, + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "block-using-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "^": schema.ListNestedBlock{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attribute": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "single_nested_block": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("single_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "^": schema.BoolAttribute{}, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "single-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.SingleNestedBlock{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-nested-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attribute": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_attribute").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-nested-invalid-field-name": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("list_nested_block").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-attribute-using-invalid-field-names": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "$": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "^": schema.Int64Attribute{}, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + "list-nested-block-with-nested-block-using-invalid-field-names": { + schema: schema.Schema{ + Blocks: map[string]schema.Block{ + "$": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "^": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "!": schema.BoolAttribute{}, + }, + }, + }, + }, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("$"), + "Invalid Schema Field Name", + `Field name "$" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^"), + "Invalid Schema Field Name", + `Field name "^" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + diag.NewAttributeErrorDiagnostic( + path.Root("$").AtName("^").AtName("!"), + "Invalid Schema Field Name", + `Field name "!" is invalid, the only allowed characters are a-z, 0-9 and _. This is always a problem with the provider and should be reported to the provider developer.`, + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +}