Skip to content

Commit a750471

Browse files
authored
list: send non-null "config" object to provider when not present in the list block (#37620)
1 parent f6ad8d3 commit a750471

File tree

13 files changed

+501
-104
lines changed

13 files changed

+501
-104
lines changed

internal/command/query_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,9 @@ func queryFixtureProvider() *testing_provider.MockProvider {
271271
},
272272
},
273273
},
274-
Nesting: configschema.NestingSingle,
274+
Nesting: configschema.NestingSingle,
275+
MinItems: 1,
276+
MaxItems: 1,
275277
},
276278
},
277279
}
@@ -292,7 +294,9 @@ func queryFixtureProvider() *testing_provider.MockProvider {
292294
},
293295
},
294296
},
295-
Nesting: configschema.NestingSingle,
297+
Nesting: configschema.NestingSingle,
298+
MinItems: 1,
299+
MaxItems: 1,
296300
},
297301
},
298302
}

internal/plugin/convert/schema.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/hashicorp/terraform/internal/configs/configschema"
1212
"github.com/hashicorp/terraform/internal/providers"
1313
proto "github.com/hashicorp/terraform/internal/tfplugin5"
14+
"github.com/zclconf/go-cty/cty"
1415
)
1516

1617
// ConfigSchemaToProto takes a *configschema.Block and converts it to a
@@ -111,6 +112,45 @@ func ProtoToActionSchema(s *proto.ActionSchema) providers.ActionSchema {
111112
}
112113
}
113114

115+
func ProtoToListSchema(s *proto.Schema) providers.Schema {
116+
listSchema := ProtoToProviderSchema(s, nil)
117+
itemCount := 0
118+
// check if the provider has set some attributes/blocks as required.
119+
// When yes, then we set minItem = 1, which
120+
// validates that the configuration contains a "config" block.
121+
for _, attrS := range listSchema.Body.Attributes {
122+
if attrS.Required {
123+
itemCount = 1
124+
break
125+
}
126+
}
127+
for _, block := range listSchema.Body.BlockTypes {
128+
if block.MinItems > 0 {
129+
itemCount = 1
130+
break
131+
}
132+
}
133+
return providers.Schema{
134+
Version: s.Version,
135+
Body: &configschema.Block{
136+
Attributes: map[string]*configschema.Attribute{
137+
"data": {
138+
Type: cty.DynamicPseudoType,
139+
Computed: true,
140+
},
141+
},
142+
BlockTypes: map[string]*configschema.NestedBlock{
143+
"config": {
144+
Block: *listSchema.Body,
145+
Nesting: configschema.NestingSingle,
146+
MinItems: itemCount,
147+
MaxItems: itemCount,
148+
},
149+
},
150+
},
151+
}
152+
}
153+
114154
// ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it
115155
// to a terraform *configschema.Block.
116156
func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {

internal/plugin/grpc_provider.go

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"google.golang.org/grpc/status"
2121

2222
"github.com/hashicorp/terraform/internal/addrs"
23-
"github.com/hashicorp/terraform/internal/configs/configschema"
2423
"github.com/hashicorp/terraform/internal/logging"
2524
"github.com/hashicorp/terraform/internal/plugin/convert"
2625
"github.com/hashicorp/terraform/internal/providers"
@@ -171,24 +170,7 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
171170
}
172171

173172
for name, list := range protoResp.ListResourceSchemas {
174-
ret := convert.ProtoToProviderSchema(list, nil)
175-
resp.ListResourceTypes[name] = providers.Schema{
176-
Version: ret.Version,
177-
Body: &configschema.Block{
178-
Attributes: map[string]*configschema.Attribute{
179-
"data": {
180-
Type: cty.DynamicPseudoType,
181-
Computed: true,
182-
},
183-
},
184-
BlockTypes: map[string]*configschema.NestedBlock{
185-
"config": {
186-
Block: *ret.Body,
187-
Nesting: configschema.NestingSingle,
188-
},
189-
},
190-
},
191-
}
173+
resp.ListResourceTypes[name] = convert.ProtoToListSchema(list)
192174
}
193175

194176
for name, action := range protoResp.ActionSchemas {
@@ -381,10 +363,12 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour
381363
}
382364

383365
configSchema := listResourceSchema.Body.BlockTypes["config"]
384-
config := cty.NullVal(configSchema.ImpliedType())
385-
if r.Config.Type().HasAttribute("config") {
386-
config = r.Config.GetAttr("config")
366+
if !r.Config.Type().HasAttribute("config") {
367+
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
368+
return resp
387369
}
370+
371+
config := r.Config.GetAttr("config")
388372
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
389373
if err != nil {
390374
resp.Diagnostics = resp.Diagnostics.Append(err)
@@ -1342,10 +1326,12 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
13421326
}
13431327

13441328
configSchema := listResourceSchema.Body.BlockTypes["config"]
1345-
config := cty.NullVal(configSchema.ImpliedType())
1346-
if r.Config.Type().HasAttribute("config") {
1347-
config = r.Config.GetAttr("config")
1329+
if !r.Config.Type().HasAttribute("config") {
1330+
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
1331+
return resp
13481332
}
1333+
1334+
config := r.Config.GetAttr("config")
13491335
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
13501336
if err != nil {
13511337
resp.Diagnostics = resp.Diagnostics.Append(err)

internal/plugin/grpc_provider_test.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ func providerProtoSchema() *proto.GetProviderSchema_Response {
146146
Required: true,
147147
},
148148
},
149+
BlockTypes: []*proto.Schema_NestedBlock{
150+
{
151+
TypeName: "nested_filter",
152+
Nesting: proto.Schema_NestedBlock_SINGLE,
153+
Block: &proto.Schema_Block{
154+
Attributes: []*proto.Schema_Attribute{
155+
{
156+
Name: "nested_attr",
157+
Type: []byte(`"string"`),
158+
Required: false,
159+
},
160+
},
161+
},
162+
MinItems: 1,
163+
MaxItems: 1,
164+
},
165+
},
149166
},
150167
},
151168
},
@@ -466,7 +483,7 @@ func TestGRPCProvider_ValidateListResourceConfig(t *testing.T) {
466483
gomock.Any(),
467484
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
468485

469-
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value"}})
486+
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value", "nested_filter": map[string]interface{}{"nested_attr": "value"}}})
470487
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
471488
TypeName: "list",
472489
Config: cfg,
@@ -478,8 +495,18 @@ func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
478495
ctrl := gomock.NewController(t)
479496
client := mockproto.NewMockProviderClient(ctrl)
480497
sch := providerProtoSchema()
481-
sch.ListResourceSchemas["list"].Block.Attributes[0].Optional = true
482-
sch.ListResourceSchemas["list"].Block.Attributes[0].Required = false
498+
499+
// mock the schema in a way that makes the config attributes optional
500+
listSchema := sch.ListResourceSchemas["list"].Block
501+
// filter_attr is optional
502+
listSchema.Attributes[0].Optional = true
503+
listSchema.Attributes[0].Required = false
504+
505+
// nested_filter is optional
506+
listSchema.BlockTypes[0].MinItems = 0
507+
listSchema.BlockTypes[0].MaxItems = 0
508+
509+
sch.ListResourceSchemas["list"].Block = listSchema
483510
// we always need a GetSchema method
484511
client.EXPECT().GetSchema(
485512
gomock.Any(),
@@ -502,10 +529,15 @@ func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
502529
gomock.Any(),
503530
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
504531

505-
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{})
532+
converted := convert.ProtoToListSchema(sch.ListResourceSchemas["list"])
533+
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]any{})
534+
coercedCfg, err := converted.Body.CoerceValue(cfg)
535+
if err != nil {
536+
t.Fatalf("failed to coerce config: %v", err)
537+
}
506538
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
507539
TypeName: "list",
508-
Config: cfg,
540+
Config: coercedCfg,
509541
})
510542
checkDiags(t, resp.Diagnostics)
511543
}
@@ -1438,8 +1470,25 @@ func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
14381470
Required: true,
14391471
},
14401472
},
1473+
BlockTypes: map[string]*configschema.NestedBlock{
1474+
"nested_filter": {
1475+
Block: configschema.Block{
1476+
Attributes: map[string]*configschema.Attribute{
1477+
"nested_attr": {
1478+
Type: cty.String,
1479+
Required: false,
1480+
},
1481+
},
1482+
},
1483+
Nesting: configschema.NestingSingle,
1484+
MinItems: 1,
1485+
MaxItems: 1,
1486+
},
1487+
},
14411488
},
1442-
Nesting: configschema.NestingSingle,
1489+
Nesting: configschema.NestingSingle,
1490+
MinItems: 1,
1491+
MaxItems: 1,
14431492
},
14441493
},
14451494
},
@@ -1485,6 +1534,9 @@ func TestGRPCProvider_Encode(t *testing.T) {
14851534
Before: cty.NullVal(cty.Object(map[string]cty.Type{
14861535
"config": cty.Object(map[string]cty.Type{
14871536
"filter_attr": cty.String,
1537+
"nested_filter": cty.Object(map[string]cty.Type{
1538+
"nested_attr": cty.String,
1539+
}),
14881540
}),
14891541
"data": cty.List(cty.Object(map[string]cty.Type{
14901542
"state": cty.Object(map[string]cty.Type{
@@ -1498,6 +1550,9 @@ func TestGRPCProvider_Encode(t *testing.T) {
14981550
After: cty.ObjectVal(map[string]cty.Value{
14991551
"config": cty.ObjectVal(map[string]cty.Value{
15001552
"filter_attr": cty.StringVal("value"),
1553+
"nested_filter": cty.ObjectVal(map[string]cty.Value{
1554+
"nested_attr": cty.StringVal("value"),
1555+
}),
15011556
}),
15021557
"data": cty.ListVal([]cty.Value{
15031558
cty.ObjectVal(map[string]cty.Value{
@@ -1649,6 +1704,9 @@ func TestGRPCProvider_ListResource(t *testing.T) {
16491704
configVal := cty.ObjectVal(map[string]cty.Value{
16501705
"config": cty.ObjectVal(map[string]cty.Value{
16511706
"filter_attr": cty.StringVal("filter-value"),
1707+
"nested_filter": cty.ObjectVal(map[string]cty.Value{
1708+
"nested_attr": cty.StringVal("value"),
1709+
}),
16521710
}),
16531711
})
16541712
request := providers.ListResourceRequest{
@@ -1731,6 +1789,9 @@ func TestGRPCProvider_ListResource_Error(t *testing.T) {
17311789
configVal := cty.ObjectVal(map[string]cty.Value{
17321790
"config": cty.ObjectVal(map[string]cty.Value{
17331791
"filter_attr": cty.StringVal("filter-value"),
1792+
"nested_filter": cty.ObjectVal(map[string]cty.Value{
1793+
"nested_attr": cty.StringVal("value"),
1794+
}),
17341795
}),
17351796
})
17361797
request := providers.ListResourceRequest{
@@ -1746,6 +1807,9 @@ func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
17461807
configVal := cty.ObjectVal(map[string]cty.Value{
17471808
"config": cty.ObjectVal(map[string]cty.Value{
17481809
"filter_attr": cty.StringVal("filter-value"),
1810+
"nested_filter": cty.ObjectVal(map[string]cty.Value{
1811+
"nested_attr": cty.StringVal("value"),
1812+
}),
17491813
}),
17501814
})
17511815
request := providers.ListResourceRequest{
@@ -2009,6 +2073,9 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
20092073
configVal := cty.ObjectVal(map[string]cty.Value{
20102074
"config": cty.ObjectVal(map[string]cty.Value{
20112075
"filter_attr": cty.StringVal("filter-value"),
2076+
"nested_filter": cty.ObjectVal(map[string]cty.Value{
2077+
"nested_attr": cty.StringVal("value"),
2078+
}),
20122079
}),
20132080
})
20142081
request := providers.ListResourceRequest{

internal/plugin6/convert/schema.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,45 @@ func ProtoToActionSchema(s *proto.ActionSchema) providers.ActionSchema {
118118
}
119119
}
120120

121+
func ProtoToListSchema(s *proto.Schema) providers.Schema {
122+
listSchema := ProtoToProviderSchema(s, nil)
123+
itemCount := 0
124+
// check if the provider has set some attributes/blocks as required.
125+
// When yes, then we set minItem = 1, which
126+
// validates that the configuration contains a "config" block.
127+
for _, attrS := range listSchema.Body.Attributes {
128+
if attrS.Required {
129+
itemCount = 1
130+
break
131+
}
132+
}
133+
for _, block := range listSchema.Body.BlockTypes {
134+
if block.MinItems > 0 {
135+
itemCount = 1
136+
break
137+
}
138+
}
139+
return providers.Schema{
140+
Version: listSchema.Version,
141+
Body: &configschema.Block{
142+
Attributes: map[string]*configschema.Attribute{
143+
"data": {
144+
Type: cty.DynamicPseudoType,
145+
Computed: true,
146+
},
147+
},
148+
BlockTypes: map[string]*configschema.NestedBlock{
149+
"config": {
150+
Block: *listSchema.Body,
151+
Nesting: configschema.NestingSingle,
152+
MinItems: itemCount,
153+
MaxItems: itemCount,
154+
},
155+
},
156+
},
157+
}
158+
}
159+
121160
func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object {
122161
obj := &configschema.Object{
123162
Attributes: make(map[string]*configschema.Attribute),

0 commit comments

Comments
 (0)