Skip to content

Commit

Permalink
resource/schema: Initial package (#558)
Browse files Browse the repository at this point in the history
Reference: #132
Reference: #326
Reference: #437
Reference: #491
Reference: #508
Reference: #532

This change introduces a new `resource/schema` package, which contains schema interfaces and types relevant to resources. This new schema implementation also provides strongly typed attributes, nested attributes, and blocks with customizable types. Nested attributes and blocks are exposed with a separate nested object for customization, plan modification, and validation.

The implementation leans heavily on the design choice of the framework being responsible for preventing provider developer runtime errors. The tailored fields no longer expose functionality that is not available for resources. The framework design will also raise compiler-time errors for errant typing of validators.

No changes are required for data handling in any other `resource.Resource` methods.

Example definition:

```go
package test

import (
	"context"

	"github.com/bflad/terraform-plugin-framework-type-time/timetypes"
	"github.com/hashicorp/terraform-plugin-framework-validators/float64validator"
	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

type ThingResource struct{}

func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"string_attribute": schema.StringAttribute{
				Required: true,
				Validators: []validator.String{
					stringvalidator.LengthBetween(3, 256),
				},
			},
			"custom_string_attribute": schema.StringAttribute{
				CustomType: timetypes.RFC3339Type,
				Optional:   true,
			},
			"list_attribute": schema.ListAttribute{
				ElementType: types.StringType,
				Optional:    true,
			},
			"list_nested_attribute": schema.ListNestedAttribute{
				NestedObject: schema.NestedAttributeObject{
					Attributes: map[string]schema.Attribute{
						"bool_attribute": schema.BoolAttribute{
							Optional: true,
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Optional: true,
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"int64_attribute": schema.Int64Attribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
		Blocks: map[string]schema.Block{
			"list_block": schema.ListNestedBlock{
				NestedObject: schema.NestedBlockObject{
					Attributes: map[string]schema.Attribute{
						"float64_attribute": schema.Float64Attribute{
							Optional: true,
							Validators: []validator.Float64{
								float64validator.OneOf(1.2, 2.4),
							},
						},
					},
					Validators: []validator.Object{ /*...*/ },
				},
				Validators: []validator.List{
					listvalidator.SizeAtMost(2),
				},
			},
		},
	}
}
```

To migrate a resource schema:

- Add `github.com/hashicorp/terraform-plugin-framework/resource/schema` to the `import` statement
- Switch the `resource.Resource` implementation `GetSchema` method to `Schema` whose response includes a `schema.Schema` from the new package.

Prior implementation:

```go
func (r ThingResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
  return tfsdk.Schema{/* ... */}, nil
}
```

Migrated implementation:

```go
func (r ThingResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
  resp.Schema = schema.Schema{/*...*/}
}
```

If the resource requires no schema, the method can be entirely empty.

- Switch `map[string]tfsdk.Attribute` with `map[string]schema.Attribute`
- Switch `map[string]tfsdk.Block` with `map[string]schema.Block`
- Switch individual attribute and block definitions. Unless the code was already taking advantage of custom attribute types (uncommon so far), the `Type` field will be removed and the map entries must declare the typed implementation, e.g. a `tfsdk.Attribute` with `Type: types.StringType` is equivalent to `schema.StringAttribute`. Custom attribute types can be specified via the `CustomType` field in each of the implementations.

Prior primitive type (`types.BoolType`, `types.Float64Type`, `types.Int64Type`, `types.NumberType`, `types.StringType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.StringType,
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.StringAttribute{
  Required: true,
}
```

Prior collection type (`types.ListType`, `types.MapType`, `types.SetType`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Required: true,
  Type: types.ListType{
    ElemType: types.StringType,
  },
}
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListAttribute{
  ElementType: types.StringType,
  Required: true,
}
```

Prior single nested attributes type (`tfsdk.SingleNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.SingleNestedAttribute{
  Attributes: map[string]schema.Attribute{/*...*/},
  Required: true,
}
```

Prior collection nested attributes type (`tfsdk.ListNestedAttributes()`, `tfsdk.MapNestedAttributes()`, `tfsdk.SetNestedAttributes()`) attribute implementation:

```go
// The "tfsdk.Attribute" could be omitted inside a map[string]tfsdk.Attribute
tfsdk.Attribute{
  Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{/*...*/}),
  Required: true,
},
```

Migrated implementation:

```go
// The schema.XXXAttribute must be declared inside map[string]schema.Attribute
schema.ListNestedAttribute{
  NestedObject: schema.NestedAttributeObject{
    Attributes: map[string]schema.Attribute{/*...*/},
  },
  Required: true,
}
```

Prior collection blocks type (`tfsdk.Block`) attribute implementation:

```go
// The "tfsdk.Block" could be omitted inside a map[string]tfsdk.Block
tfsdk.Block{
  Attributes: map[string]tfsdk.Attribute{/*...*/},
  Blocks: map[string]tfsdk.Block{/*...*/},
  NestingMode: tfsdk.BlockNestingModeList,
},
```

Migrated implementation:

```go
// The schema.XXXBlock must be declared inside map[string]schema.Block
schema.ListNestedBlock{
  NestedObject: schema.NestedBlockObject{
    Attributes: map[string]schema.Attribute{/*...*/},
    Blocks: map[string]schema.Block{/*...*/},
  },
}
```
  • Loading branch information
bflad committed Nov 29, 2022
1 parent 28f4804 commit 30b78ab
Show file tree
Hide file tree
Showing 64 changed files with 14,562 additions and 527 deletions.
7 changes: 7 additions & 0 deletions .changelog/558.txt
@@ -0,0 +1,7 @@
```release-note:note
resource: The `Resource` type `GetSchema` method has been deprecated. Use the `Schema` method instead.
```

```release-note:feature
resource/schema: New package which contains schema interfaces and types relevant to resources
```
41 changes: 33 additions & 8 deletions internal/fwserver/server.go
Expand Up @@ -452,17 +452,42 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem
for resourceTypeName, resourceFunc := range resourceFuncs {
res := resourceFunc()

logging.FrameworkDebug(ctx, "Calling provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
schema, diags := res.GetSchema(ctx)
logging.FrameworkDebug(ctx, "Called provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
switch resourceIface := res.(type) {
case resource.ResourceWithSchema:
schemaReq := resource.SchemaRequest{}
schemaResp := resource.SchemaResponse{}

s.resourceSchemasDiags.Append(diags...)
logging.FrameworkDebug(ctx, "Calling provider defined Resource Schema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
resourceIface.Schema(ctx, schemaReq, &schemaResp)
logging.FrameworkDebug(ctx, "Called provider defined Resource Schema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})

if s.resourceSchemasDiags.HasError() {
return s.resourceSchemas, s.resourceSchemasDiags
}
s.resourceSchemasDiags.Append(schemaResp.Diagnostics...)

if s.resourceSchemasDiags.HasError() {
return s.resourceSchemas, s.resourceSchemasDiags
}

s.resourceSchemas[resourceTypeName] = schema
s.resourceSchemas[resourceTypeName] = schemaResp.Schema
case resource.ResourceWithGetSchema:
logging.FrameworkDebug(ctx, "Calling provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})
schema, diags := resourceIface.GetSchema(ctx) //nolint:staticcheck // Required internal usage until removal
logging.FrameworkDebug(ctx, "Called provider defined Resource GetSchema", map[string]interface{}{logging.KeyResourceType: resourceTypeName})

s.resourceSchemasDiags.Append(diags...)

if s.resourceSchemasDiags.HasError() {
return s.resourceSchemas, s.resourceSchemasDiags
}

s.resourceSchemas[resourceTypeName] = schema
default:
s.resourceSchemasDiags.AddError(
"Resource Missing Schema",
"While attempting to load provider resource schemas, a resource was missing a Schema method. "+
"This is always an issue in the provider and should be reported to the provider developers.\n\n"+
"Resource Type Name: "+resourceTypeName,
)
}
}

return s.resourceSchemas, s.resourceSchemasDiags
Expand Down
77 changes: 35 additions & 42 deletions internal/fwserver/server_getproviderschema_test.go
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/provider"
providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)
Expand Down Expand Up @@ -316,15 +317,14 @@ func TestServerGetProviderSchema(t *testing.T) {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test1": {
SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test1": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
}, nil
}
},
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource1"
Expand All @@ -333,15 +333,14 @@ func TestServerGetProviderSchema(t *testing.T) {
},
func() resource.Resource {
return &testprovider.Resource{
GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test2": {
SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test2": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
}, nil
}
},
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource2"
Expand All @@ -357,19 +356,17 @@ func TestServerGetProviderSchema(t *testing.T) {
DataSourceSchemas: map[string]fwschema.Schema{},
Provider: providerschema.Schema{},
ResourceSchemas: map[string]fwschema.Schema{
"test_resource1": tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test1": {
"test_resource1": resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test1": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
},
"test_resource2": tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test2": {
"test_resource2": resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test2": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
},
Expand All @@ -386,15 +383,14 @@ func TestServerGetProviderSchema(t *testing.T) {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test1": {
SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test1": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
}, nil
}
},
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource"
Expand All @@ -403,15 +399,14 @@ func TestServerGetProviderSchema(t *testing.T) {
},
func() resource.Resource {
return &testprovider.Resource{
GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test2": {
SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test2": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
}, nil
}
},
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource"
Expand Down Expand Up @@ -484,15 +479,14 @@ func TestServerGetProviderSchema(t *testing.T) {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
GetSchemaMethod: func(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test": {
SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
}, nil
}
},
MetadataMethod: func(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_resource"
Expand All @@ -509,11 +503,10 @@ func TestServerGetProviderSchema(t *testing.T) {
DataSourceSchemas: map[string]fwschema.Schema{},
Provider: providerschema.Schema{},
ResourceSchemas: map[string]fwschema.Schema{
"testprovidertype_resource": tfsdk.Schema{
Attributes: map[string]tfsdk.Attribute{
"test": {
"testprovidertype_resource": resourceschema.Schema{
Attributes: map[string]resourceschema.Attribute{
"test": resourceschema.StringAttribute{
Required: true,
Type: types.StringType,
},
},
},
Expand Down

0 comments on commit 30b78ab

Please sign in to comment.