Skip to content

Commit 1f49968

Browse files
authored
helper/schema: Add SchemaFunc field to Resource type (#1218)
Reference: hashicorp/terraform-plugin-sdk#1217 This change introduces a new `SchemaFunc` field and `SchemaMap` method to the `Resource` type. Currently, the `Schema` field data of all `Resource` is held in memory for the lifecycle of the provider server, which is problematic for providers with many resources and/or larger schemas in resources. The new field enables provider developers to swap pieces of resident memory usage for the slight additional computation time of reconstituting the data when necessary. Callers directly referencing the exported `Schema` field should switch to referencing the `SchemaMap` method, which returns the result of `SchemaFunc` or `Schema` in that preference order. To ensure internal usage was migrated to the new method, this change was performed by temporarily commenting out the `Schema` field itself with broken references in non-testing code migrated to the method. The `Schema` field is not marked as deprecated via Go documentation comment as this would introduce a major ecosystem burden to migrate with generally little benefit for most use cases. The `Resource` type `InternalValidate` method has been updated to return an error if both `Schema` and `SchemaFunc` are defined. Provider developers are encouraged to migrate resources to the terraform-plugin-framework, as it already behaves in a manner similar to `SchemaFunc` by nature of resource schema data being behind a method call, amongst many of the other benefits outlined at https://developer.hashicorp.com/terraform/plugin/framework-benefits.
1 parent 762614f commit 1f49968

File tree

12 files changed

+177
-30
lines changed

12 files changed

+177
-30
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'helper/schema: Added `Resource` type `SchemaFunc` field and `SchemaMap` method,
3+
which can reduce resident memory usage with large schemas'
4+
time: 2023-06-23T10:24:12.025356-04:00
5+
custom:
6+
Issue: "1217"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: NOTES
2+
body: 'helper/schema: Consumers directly referencing the `Resource` type `Schema`
3+
field should switch to the `SchemaMap` method to ensure new `SchemaFunc` field
4+
data is properly retrieved'
5+
time: 2023-06-23T10:25:28.864812-04:00
6+
custom:
7+
Issue: "1217"

helper/schema/core_schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,5 +367,5 @@ func (r *Resource) CoreConfigSchema() *configschema.Block {
367367
}
368368

369369
func (r *Resource) coreConfigSchema() *configschema.Block {
370-
return schemaMap(r.Schema).CoreConfigSchema()
370+
return schemaMap(r.SchemaMap()).CoreConfigSchema()
371371
}

helper/schema/field_reader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
8484
case *Resource:
8585
current = &Schema{
8686
Type: typeObject,
87-
Elem: v.Schema,
87+
Elem: v.SchemaMap(),
8888
}
8989
case *Schema:
9090
current = v

helper/schema/field_reader_config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool
303303

304304
switch t := schema.Elem.(type) {
305305
case *Resource:
306-
for k, schema := range t.Schema {
306+
for k, schema := range t.SchemaMap() {
307307
if r.Config.IsComputed(prefix + k) {
308308
return true
309309
}

helper/schema/grpc_provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,7 @@ func stripResourceModifiers(r *Resource) *Resource {
13031303
newResource.CustomizeDiff = nil
13041304
newResource.Schema = map[string]*Schema{}
13051305

1306-
for k, s := range r.Schema {
1306+
for k, s := range r.SchemaMap() {
13071307
newResource.Schema[k] = stripSchema(s)
13081308
}
13091309

helper/schema/grpc_provider_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,23 @@ func TestApplyResourceChange(t *testing.T) {
807807
},
808808
},
809809
},
810+
{
811+
Description: "CreateContext_SchemaFunc",
812+
TestResource: &Resource{
813+
SchemaFunc: func() map[string]*Schema {
814+
return map[string]*Schema{
815+
"id": {
816+
Type: TypeString,
817+
Computed: true,
818+
},
819+
}
820+
},
821+
CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics {
822+
rd.SetId("bar") // expected in response
823+
return nil
824+
},
825+
},
826+
},
810827
}
811828

812829
for _, testCase := range testCases {
@@ -1254,6 +1271,47 @@ func TestReadDataSource(t *testing.T) {
12541271
},
12551272
},
12561273
},
1274+
"SchemaFunc": {
1275+
server: NewGRPCProviderServer(&Provider{
1276+
DataSourcesMap: map[string]*Resource{
1277+
"test": {
1278+
SchemaFunc: func() map[string]*Schema {
1279+
return map[string]*Schema{
1280+
"id": {
1281+
Type: TypeString,
1282+
Computed: true,
1283+
},
1284+
}
1285+
},
1286+
ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics {
1287+
d.SetId("test-id")
1288+
return nil
1289+
},
1290+
},
1291+
},
1292+
}),
1293+
req: &tfprotov5.ReadDataSourceRequest{
1294+
TypeName: "test",
1295+
Config: &tfprotov5.DynamicValue{
1296+
MsgPack: mustMsgpackMarshal(
1297+
cty.EmptyObject,
1298+
cty.NullVal(cty.EmptyObject),
1299+
),
1300+
},
1301+
},
1302+
expected: &tfprotov5.ReadDataSourceResponse{
1303+
State: &tfprotov5.DynamicValue{
1304+
MsgPack: mustMsgpackMarshal(
1305+
cty.Object(map[string]cty.Type{
1306+
"id": cty.String,
1307+
}),
1308+
cty.ObjectVal(map[string]cty.Value{
1309+
"id": cty.StringVal("test-id"),
1310+
}),
1311+
),
1312+
},
1313+
},
1314+
},
12571315
"null-object": {
12581316
server: NewGRPCProviderServer(&Provider{
12591317
DataSourcesMap: map[string]*Resource{

helper/schema/resource.go

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,25 @@ var ReservedResourceFields = []string{
5656
// being implemented.
5757
type Resource struct {
5858
// Schema is the structure and type information for this component. This
59-
// field is required for all Resource concepts.
59+
// field, or SchemaFunc, is required for all Resource concepts. To prevent
60+
// storing all schema information in memory for the lifecycle of a provider,
61+
// use SchemaFunc instead.
6062
//
6163
// The keys of this map are the names used in a practitioner configuration,
6264
// such as the attribute or block name. The values describe the structure
6365
// and type information of that attribute or block.
6466
Schema map[string]*Schema
6567

68+
// SchemaFunc is the structure and type information for this component. This
69+
// field, or Schema, is required for all Resource concepts. Use this field
70+
// instead of Schema on top level Resource declarations to prevent storing
71+
// all schema information in memory for the lifecycle of a provider.
72+
//
73+
// The keys of this map are the names used in a practitioner configuration,
74+
// such as the attribute or block name. The values describe the structure
75+
// and type information of that attribute or block.
76+
SchemaFunc func() map[string]*Schema
77+
6678
// SchemaVersion is the version number for this resource's Schema
6779
// definition. This field is only valid when the Resource is a managed
6880
// resource.
@@ -585,6 +597,17 @@ type Resource struct {
585597
UseJSONNumber bool
586598
}
587599

600+
// SchemaMap returns the schema information for this Resource whether it is
601+
// defined via the SchemaFunc field or Schema field. The SchemaFunc field, if
602+
// defined, takes precedence over the Schema field.
603+
func (r *Resource) SchemaMap() map[string]*Schema {
604+
if r.SchemaFunc != nil {
605+
return r.SchemaFunc()
606+
}
607+
608+
return r.Schema
609+
}
610+
588611
// ShimInstanceStateFromValue converts a cty.Value to a
589612
// terraform.InstanceState.
590613
func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.InstanceState, error) {
@@ -594,7 +617,7 @@ func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*terraform.Insta
594617

595618
// We now rebuild the state through the ResourceData, so that the set indexes
596619
// match what helper/schema expects.
597-
data, err := schemaMap(r.Schema).Data(s, nil)
620+
data, err := schemaMap(r.SchemaMap()).Data(s, nil)
598621
if err != nil {
599622
return nil, err
600623
}
@@ -767,7 +790,8 @@ func (r *Resource) Apply(
767790
s *terraform.InstanceState,
768791
d *terraform.InstanceDiff,
769792
meta interface{}) (*terraform.InstanceState, diag.Diagnostics) {
770-
data, err := schemaMap(r.Schema).Data(s, d)
793+
schema := schemaMap(r.SchemaMap())
794+
data, err := schema.Data(s, d)
771795
if err != nil {
772796
return s, diag.FromErr(err)
773797
}
@@ -824,7 +848,7 @@ func (r *Resource) Apply(
824848
}
825849

826850
// Reset the data to be stateless since we just destroyed
827-
data, err = schemaMap(r.Schema).Data(nil, d)
851+
data, err = schema.Data(nil, d)
828852
if err != nil {
829853
return nil, append(diags, diag.FromErr(err)...)
830854
}
@@ -868,7 +892,7 @@ func (r *Resource) Diff(
868892
return nil, fmt.Errorf("[ERR] Error decoding timeout: %s", err)
869893
}
870894

871-
instanceDiff, err := schemaMap(r.Schema).Diff(ctx, s, c, r.CustomizeDiff, meta, true)
895+
instanceDiff, err := schemaMap(r.SchemaMap()).Diff(ctx, s, c, r.CustomizeDiff, meta, true)
872896
if err != nil {
873897
return instanceDiff, err
874898
}
@@ -890,7 +914,7 @@ func (r *Resource) SimpleDiff(
890914
c *terraform.ResourceConfig,
891915
meta interface{}) (*terraform.InstanceDiff, error) {
892916

893-
instanceDiff, err := schemaMap(r.Schema).Diff(ctx, s, c, r.CustomizeDiff, meta, false)
917+
instanceDiff, err := schemaMap(r.SchemaMap()).Diff(ctx, s, c, r.CustomizeDiff, meta, false)
894918
if err != nil {
895919
return instanceDiff, err
896920
}
@@ -915,7 +939,7 @@ func (r *Resource) SimpleDiff(
915939

916940
// Validate validates the resource configuration against the schema.
917941
func (r *Resource) Validate(c *terraform.ResourceConfig) diag.Diagnostics {
918-
diags := schemaMap(r.Schema).Validate(c)
942+
diags := schemaMap(r.SchemaMap()).Validate(c)
919943

920944
if r.DeprecationMessage != "" {
921945
diags = append(diags, diag.Diagnostic{
@@ -937,7 +961,7 @@ func (r *Resource) ReadDataApply(
937961
) (*terraform.InstanceState, diag.Diagnostics) {
938962
// Data sources are always built completely from scratch
939963
// on each read, so the source state is always nil.
940-
data, err := schemaMap(r.Schema).Data(nil, d)
964+
data, err := schemaMap(r.SchemaMap()).Data(nil, d)
941965
if err != nil {
942966
return nil, diag.FromErr(err)
943967
}
@@ -978,10 +1002,12 @@ func (r *Resource) RefreshWithoutUpgrade(
9781002
}
9791003
}
9801004

1005+
schema := schemaMap(r.SchemaMap())
1006+
9811007
if r.Exists != nil {
9821008
// Make a copy of data so that if it is modified it doesn't
9831009
// affect our Read later.
984-
data, err := schemaMap(r.Schema).Data(s, nil)
1010+
data, err := schema.Data(s, nil)
9851011
if err != nil {
9861012
return s, diag.FromErr(err)
9871013
}
@@ -1004,7 +1030,7 @@ func (r *Resource) RefreshWithoutUpgrade(
10041030
}
10051031
}
10061032

1007-
data, err := schemaMap(r.Schema).Data(s, nil)
1033+
data, err := schema.Data(s, nil)
10081034
if err != nil {
10091035
return s, diag.FromErr(err)
10101036
}
@@ -1023,7 +1049,7 @@ func (r *Resource) RefreshWithoutUpgrade(
10231049
state = nil
10241050
}
10251051

1026-
schemaMap(r.Schema).handleDiffSuppressOnRefresh(ctx, s, state)
1052+
schema.handleDiffSuppressOnRefresh(ctx, s, state)
10271053
return r.recordCurrentSchemaVersion(state), diags
10281054
}
10291055

@@ -1069,13 +1095,14 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
10691095
}
10701096
}
10711097

1098+
schema := schemaMap(r.SchemaMap())
10721099
tsm := topSchemaMap
10731100

10741101
if r.isTopLevel() && writable {
10751102
// All non-Computed attributes must be ForceNew if Update is not defined
10761103
if !r.updateFuncSet() {
10771104
nonForceNewAttrs := make([]string, 0)
1078-
for k, v := range r.Schema {
1105+
for k, v := range schema {
10791106
if !v.ForceNew && !v.Computed {
10801107
nonForceNewAttrs = append(nonForceNewAttrs, k)
10811108
}
@@ -1086,19 +1113,19 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
10861113
}
10871114
} else {
10881115
nonUpdateableAttrs := make([]string, 0)
1089-
for k, v := range r.Schema {
1116+
for k, v := range schema {
10901117
if v.ForceNew || v.Computed && !v.Optional {
10911118
nonUpdateableAttrs = append(nonUpdateableAttrs, k)
10921119
}
10931120
}
1094-
updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
1121+
updateableAttrs := len(schema) - len(nonUpdateableAttrs)
10951122
if updateableAttrs == 0 {
10961123
return fmt.Errorf(
10971124
"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
10981125
}
10991126
}
11001127

1101-
tsm = schemaMap(r.Schema)
1128+
tsm = schema
11021129

11031130
// Destroy, and Read are required
11041131
if !r.readFuncSet() {
@@ -1157,14 +1184,18 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
11571184

11581185
// Data source
11591186
if r.isTopLevel() && !writable {
1160-
tsm = schemaMap(r.Schema)
1187+
tsm = schema
11611188
for k := range tsm {
11621189
if isReservedDataSourceFieldName(k) {
11631190
return fmt.Errorf("%s is a reserved field name", k)
11641191
}
11651192
}
11661193
}
11671194

1195+
if r.SchemaFunc != nil && r.Schema != nil {
1196+
return fmt.Errorf("SchemaFunc and Schema should not both be set")
1197+
}
1198+
11681199
// check context funcs are not set alongside their nonctx counterparts
11691200
if r.CreateContext != nil && r.Create != nil {
11701201
return fmt.Errorf("CreateContext and Create should not both be set")
@@ -1207,7 +1238,7 @@ func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error
12071238
return fmt.Errorf("Delete and DeleteWithoutTimeout should not both be set")
12081239
}
12091240

1210-
return schemaMap(r.Schema).InternalValidate(tsm)
1241+
return schema.InternalValidate(tsm)
12111242
}
12121243

12131244
func isReservedDataSourceFieldName(name string) bool {
@@ -1254,7 +1285,7 @@ func isReservedResourceFieldName(name string) bool {
12541285
//
12551286
// This function is useful for unit tests and ResourceImporter functions.
12561287
func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
1257-
result, err := schemaMap(r.Schema).Data(s, nil)
1288+
result, err := schemaMap(r.SchemaMap()).Data(s, nil)
12581289
if err != nil {
12591290
// At the time of writing, this isn't possible (Data never returns
12601291
// non-nil errors). We panic to find this in the future if we have to.
@@ -1281,7 +1312,7 @@ func (r *Resource) Data(s *terraform.InstanceState) *ResourceData {
12811312
// TODO: May be able to be removed with the above ResourceData function.
12821313
func (r *Resource) TestResourceData() *ResourceData {
12831314
return &ResourceData{
1284-
schema: r.Schema,
1315+
schema: r.SchemaMap(),
12851316
}
12861317
}
12871318

helper/schema/resource_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,51 @@ func TestResourceInternalValidate(t *testing.T) {
10801080
true,
10811081
true,
10821082
},
1083+
27: { // Non-Writable SchemaFunc and Schema should not both be set
1084+
In: &Resource{
1085+
Schema: map[string]*Schema{
1086+
"test": {
1087+
Type: TypeString,
1088+
Required: true,
1089+
},
1090+
},
1091+
SchemaFunc: func() map[string]*Schema {
1092+
return map[string]*Schema{
1093+
"test": {
1094+
Type: TypeString,
1095+
Required: true,
1096+
},
1097+
}
1098+
},
1099+
Read: Noop,
1100+
},
1101+
Writable: false,
1102+
Err: true,
1103+
},
1104+
28: { // Writable SchemaFunc and Schema should not both be set
1105+
In: &Resource{
1106+
Schema: map[string]*Schema{
1107+
"test": {
1108+
Type: TypeString,
1109+
Required: true,
1110+
},
1111+
},
1112+
SchemaFunc: func() map[string]*Schema {
1113+
return map[string]*Schema{
1114+
"test": {
1115+
Type: TypeString,
1116+
Required: true,
1117+
},
1118+
}
1119+
},
1120+
Create: Noop,
1121+
Read: Noop,
1122+
Update: Noop,
1123+
Delete: Noop,
1124+
},
1125+
Writable: true,
1126+
Err: true,
1127+
},
10831128
}
10841129

10851130
for i, tc := range cases {

0 commit comments

Comments
 (0)