list: send non-null "config" object to provider when not present in the list block (#37620) (#37662)

pull/37661/head
github-actions[bot] 8 months ago committed by GitHub
parent 6bb50f4056
commit 20ca183851
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -270,7 +270,9 @@ func queryFixtureProvider() *testing_provider.MockProvider {
},
},
},
Nesting: configschema.NestingSingle,
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
},
}
@ -291,7 +293,9 @@ func queryFixtureProvider() *testing_provider.MockProvider {
},
},
},
Nesting: configschema.NestingSingle,
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
},
}

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
"github.com/zclconf/go-cty/cty"
)
// ConfigSchemaToProto takes a *configschema.Block and converts it to a
@ -111,6 +112,45 @@ func ProtoToActionSchema(s *proto.ActionSchema) providers.ActionSchema {
}
}
func ProtoToListSchema(s *proto.Schema) providers.Schema {
listSchema := ProtoToProviderSchema(s, nil)
itemCount := 0
// check if the provider has set some attributes/blocks as required.
// When yes, then we set minItem = 1, which
// validates that the configuration contains a "config" block.
for _, attrS := range listSchema.Body.Attributes {
if attrS.Required {
itemCount = 1
break
}
}
for _, block := range listSchema.Body.BlockTypes {
if block.MinItems > 0 {
itemCount = 1
break
}
}
return providers.Schema{
Version: s.Version,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: *listSchema.Body,
Nesting: configschema.NestingSingle,
MinItems: itemCount,
MaxItems: itemCount,
},
},
},
}
}
// ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it
// to a terraform *configschema.Block.
func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {

@ -20,7 +20,6 @@ import (
"google.golang.org/grpc/status"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/plugin/convert"
"github.com/hashicorp/terraform/internal/providers"
@ -171,24 +170,7 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
}
for name, list := range protoResp.ListResourceSchemas {
ret := convert.ProtoToProviderSchema(list, nil)
resp.ListResourceTypes[name] = providers.Schema{
Version: ret.Version,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: *ret.Body,
Nesting: configschema.NestingSingle,
},
},
},
}
resp.ListResourceTypes[name] = convert.ProtoToListSchema(list)
}
for name, action := range protoResp.ActionSchemas {
@ -381,10 +363,12 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour
}
configSchema := listResourceSchema.Body.BlockTypes["config"]
config := cty.NullVal(configSchema.ImpliedType())
if r.Config.Type().HasAttribute("config") {
config = r.Config.GetAttr("config")
if !r.Config.Type().HasAttribute("config") {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
return resp
}
config := r.Config.GetAttr("config")
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
@ -1342,10 +1326,12 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
}
configSchema := listResourceSchema.Body.BlockTypes["config"]
config := cty.NullVal(configSchema.ImpliedType())
if r.Config.Type().HasAttribute("config") {
config = r.Config.GetAttr("config")
if !r.Config.Type().HasAttribute("config") {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
return resp
}
config := r.Config.GetAttr("config")
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)

@ -146,6 +146,23 @@ func providerProtoSchema() *proto.GetProviderSchema_Response {
Required: true,
},
},
BlockTypes: []*proto.Schema_NestedBlock{
{
TypeName: "nested_filter",
Nesting: proto.Schema_NestedBlock_SINGLE,
Block: &proto.Schema_Block{
Attributes: []*proto.Schema_Attribute{
{
Name: "nested_attr",
Type: []byte(`"string"`),
Required: false,
},
},
},
MinItems: 1,
MaxItems: 1,
},
},
},
},
},
@ -466,7 +483,7 @@ func TestGRPCProvider_ValidateListResourceConfig(t *testing.T) {
gomock.Any(),
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value"}})
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value", "nested_filter": map[string]interface{}{"nested_attr": "value"}}})
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
TypeName: "list",
Config: cfg,
@ -478,8 +495,18 @@ func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
ctrl := gomock.NewController(t)
client := mockproto.NewMockProviderClient(ctrl)
sch := providerProtoSchema()
sch.ListResourceSchemas["list"].Block.Attributes[0].Optional = true
sch.ListResourceSchemas["list"].Block.Attributes[0].Required = false
// mock the schema in a way that makes the config attributes optional
listSchema := sch.ListResourceSchemas["list"].Block
// filter_attr is optional
listSchema.Attributes[0].Optional = true
listSchema.Attributes[0].Required = false
// nested_filter is optional
listSchema.BlockTypes[0].MinItems = 0
listSchema.BlockTypes[0].MaxItems = 0
sch.ListResourceSchemas["list"].Block = listSchema
// we always need a GetSchema method
client.EXPECT().GetSchema(
gomock.Any(),
@ -502,10 +529,15 @@ func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
gomock.Any(),
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{})
converted := convert.ProtoToListSchema(sch.ListResourceSchemas["list"])
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]any{})
coercedCfg, err := converted.Body.CoerceValue(cfg)
if err != nil {
t.Fatalf("failed to coerce config: %v", err)
}
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
TypeName: "list",
Config: cfg,
Config: coercedCfg,
})
checkDiags(t, resp.Diagnostics)
}
@ -1438,8 +1470,25 @@ func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"nested_filter": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"nested_attr": {
Type: cty.String,
Required: false,
},
},
},
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
},
},
Nesting: configschema.NestingSingle,
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
},
},
@ -1485,6 +1534,9 @@ func TestGRPCProvider_Encode(t *testing.T) {
Before: cty.NullVal(cty.Object(map[string]cty.Type{
"config": cty.Object(map[string]cty.Type{
"filter_attr": cty.String,
"nested_filter": cty.Object(map[string]cty.Type{
"nested_attr": cty.String,
}),
}),
"data": cty.List(cty.Object(map[string]cty.Type{
"state": cty.Object(map[string]cty.Type{
@ -1498,6 +1550,9 @@ func TestGRPCProvider_Encode(t *testing.T) {
After: cty.ObjectVal(map[string]cty.Value{
"config": cty.ObjectVal(map[string]cty.Value{
"filter_attr": cty.StringVal("value"),
"nested_filter": cty.ObjectVal(map[string]cty.Value{
"nested_attr": cty.StringVal("value"),
}),
}),
"data": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
@ -1649,6 +1704,9 @@ func TestGRPCProvider_ListResource(t *testing.T) {
configVal := cty.ObjectVal(map[string]cty.Value{
"config": cty.ObjectVal(map[string]cty.Value{
"filter_attr": cty.StringVal("filter-value"),
"nested_filter": cty.ObjectVal(map[string]cty.Value{
"nested_attr": cty.StringVal("value"),
}),
}),
})
request := providers.ListResourceRequest{
@ -1731,6 +1789,9 @@ func TestGRPCProvider_ListResource_Error(t *testing.T) {
configVal := cty.ObjectVal(map[string]cty.Value{
"config": cty.ObjectVal(map[string]cty.Value{
"filter_attr": cty.StringVal("filter-value"),
"nested_filter": cty.ObjectVal(map[string]cty.Value{
"nested_attr": cty.StringVal("value"),
}),
}),
})
request := providers.ListResourceRequest{
@ -1746,6 +1807,9 @@ func TestGRPCProvider_ListResource_Diagnostics(t *testing.T) {
configVal := cty.ObjectVal(map[string]cty.Value{
"config": cty.ObjectVal(map[string]cty.Value{
"filter_attr": cty.StringVal("filter-value"),
"nested_filter": cty.ObjectVal(map[string]cty.Value{
"nested_attr": cty.StringVal("value"),
}),
}),
})
request := providers.ListResourceRequest{
@ -2009,6 +2073,9 @@ func TestGRPCProvider_ListResource_Limit(t *testing.T) {
configVal := cty.ObjectVal(map[string]cty.Value{
"config": cty.ObjectVal(map[string]cty.Value{
"filter_attr": cty.StringVal("filter-value"),
"nested_filter": cty.ObjectVal(map[string]cty.Value{
"nested_attr": cty.StringVal("value"),
}),
}),
})
request := providers.ListResourceRequest{

@ -118,6 +118,45 @@ func ProtoToActionSchema(s *proto.ActionSchema) providers.ActionSchema {
}
}
func ProtoToListSchema(s *proto.Schema) providers.Schema {
listSchema := ProtoToProviderSchema(s, nil)
itemCount := 0
// check if the provider has set some attributes/blocks as required.
// When yes, then we set minItem = 1, which
// validates that the configuration contains a "config" block.
for _, attrS := range listSchema.Body.Attributes {
if attrS.Required {
itemCount = 1
break
}
}
for _, block := range listSchema.Body.BlockTypes {
if block.MinItems > 0 {
itemCount = 1
break
}
}
return providers.Schema{
Version: listSchema.Version,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: *listSchema.Body,
Nesting: configschema.NestingSingle,
MinItems: itemCount,
MaxItems: itemCount,
},
},
},
}
}
func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object {
obj := &configschema.Object{
Attributes: make(map[string]*configschema.Attribute),

@ -20,7 +20,6 @@ import (
"google.golang.org/grpc/status"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/plugin6/convert"
"github.com/hashicorp/terraform/internal/providers"
@ -172,24 +171,7 @@ func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
}
for name, list := range protoResp.ListResourceSchemas {
ret := convert.ProtoToProviderSchema(list, nil)
resp.ListResourceTypes[name] = providers.Schema{
Version: ret.Version,
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"data": {
Type: cty.DynamicPseudoType,
Computed: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"config": {
Block: *ret.Body,
Nesting: configschema.NestingSingle,
},
},
},
}
resp.ListResourceTypes[name] = convert.ProtoToListSchema(list)
}
for name, store := range protoResp.StateStoreSchemas {
@ -377,11 +359,14 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown list resource type %q", r.TypeName))
return resp
}
configSchema := listResourceSchema.Body.BlockTypes["config"]
config := cty.NullVal(configSchema.ImpliedType())
if r.Config.Type().HasAttribute("config") {
config = r.Config.GetAttr("config")
if !r.Config.Type().HasAttribute("config") {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
return resp
}
config := r.Config.GetAttr("config")
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
@ -1337,10 +1322,12 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
}
configSchema := listResourceSchema.Body.BlockTypes["config"]
config := cty.NullVal(configSchema.ImpliedType())
if r.Config.Type().HasAttribute("config") {
config = r.Config.GetAttr("config")
if !r.Config.Type().HasAttribute("config") {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing required attribute \"config\"; this is a bug in Terraform - please report it"))
return resp
}
config := r.Config.GetAttr("config")
mp, err := msgpack.Marshal(config, configSchema.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)

@ -522,10 +522,15 @@ func TestGRPCProvider_ValidateListResourceConfig_OptionalCfg(t *testing.T) {
gomock.Any(),
).Return(&proto.ValidateListResourceConfig_Response{}, nil)
converted := convert.ProtoToListSchema(sch.ListResourceSchemas["list"])
cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{})
coercedCfg, err := converted.Body.CoerceValue(cfg)
if err != nil {
t.Fatalf("failed to coerce config: %v", err)
}
resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{
TypeName: "list",
Config: cfg,
Config: coercedCfg,
})
checkDiags(t, resp.Diagnostics)
}
@ -1458,7 +1463,9 @@ func TestGRPCProvider_GetSchema_ListResourceTypes(t *testing.T) {
},
},
},
Nesting: configschema.NestingSingle,
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
},
},

@ -220,6 +220,19 @@ func (a ActionSchema) IsNil() bool {
return a.ConfigSchema == nil
}
type ListResourceSchema struct {
// schema for the nested "config" block.
ConfigSchema *configschema.Block
// schema for the entire block (including "config" block)
FullSchema *configschema.Block
}
// IsNil() returns true if there is no list resource schema at all.
func (l ListResourceSchema) IsNil() bool {
return l.FullSchema == nil
}
// Schema pairs a provider or resource schema with that schema's version.
// This is used to be able to upgrade the schema in UpgradeResourceState.
//

@ -47,3 +47,20 @@ func (ss ProviderSchema) SchemaForActionType(typeName string) (schema ActionSche
}
return ActionSchema{}
}
// SchemaForListResourceType attempts to find a schema for the given type. Returns an
// empty schema if none is available.
func (ss ProviderSchema) SchemaForListResourceType(typeName string) ListResourceSchema {
schema, ok := ss.ListResourceTypes[typeName]
ret := ListResourceSchema{FullSchema: schema.Body}
if !ok || schema.Body == nil {
return ret
}
// The configuration for the list block is nested within a "config" block.
configSchema, ok := schema.Body.BlockTypes["config"]
if !ok {
return ret
}
ret.ConfigSchema = &configSchema.Block
return ret
}

@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
@ -24,15 +25,16 @@ import (
func TestContext2Plan_queryList(t *testing.T) {
cases := []struct {
name string
mainConfig string
queryConfig string
generatedPath string
diagCount int
expectedErrMsg []string
assertState func(*states.State)
assertChanges func(providers.ProviderSchema, *plans.ChangesSrc)
listResourceFn func(request providers.ListResourceRequest) providers.ListResourceResponse
name string
mainConfig string
queryConfig string
generatedPath string
expectedErrMsg []string
transformSchema func(*providers.GetProviderSchemaResponse)
assertState func(*states.State)
assertValidateDiags func(t *testing.T, diags tfdiags.Diagnostics)
assertChanges func(providers.ProviderSchema, *plans.ChangesSrc)
listResourceFn func(request providers.ListResourceRequest) providers.ListResourceResponse
}{
{
name: "valid list reference - generates config",
@ -261,6 +263,186 @@ func TestContext2Plan_queryList(t *testing.T) {
}
},
},
{
name: "with empty config when it is required",
mainConfig: `
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.0"
}
}
}
`,
queryConfig: `
variable "input" {
type = string
default = "foo"
}
list "test_resource" "test" {
provider = test
}
`,
transformSchema: func(schema *providers.GetProviderSchemaResponse) {
schema.ListResourceTypes["test_resource"].Body.BlockTypes = map[string]*configschema.NestedBlock{
"config": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"filter": {
Required: true,
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"attr": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
Nesting: configschema.NestingSingle,
MinItems: 1,
MaxItems: 1,
},
}
},
assertValidateDiags: func(t *testing.T, diags tfdiags.Diagnostics) {
tfdiags.AssertDiagnosticCount(t, diags, 1)
var exp tfdiags.Diagnostics
exp = exp.Append(&hcl.Diagnostic{
Summary: "Missing config block",
Detail: "A block of type \"config\" is required here.",
Subject: diags[0].Source().Subject.ToHCL().Ptr(),
})
tfdiags.AssertDiagnosticsMatch(t, diags, exp)
},
},
{
name: "with empty optional config",
mainConfig: `
terraform {
required_providers {
test = {
source = "hashicorp/test"
version = "1.0.0"
}
}
}
`,
queryConfig: `
variable "input" {
type = string
default = "foo"
}
list "test_resource" "test" {
provider = test
}
`,
transformSchema: func(schema *providers.GetProviderSchemaResponse) {
schema.ListResourceTypes["test_resource"].Body.BlockTypes = map[string]*configschema.NestedBlock{
"config": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"filter": {
Optional: true,
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"attr": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
Nesting: configschema.NestingSingle,
},
}
},
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
madeUp := []cty.Value{
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}),
cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-654321")}),
}
ids := []cty.Value{}
for i := range madeUp {
ids = append(ids, cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(fmt.Sprintf("i-v%d", i+1)),
}))
}
resp := []cty.Value{}
for i, v := range madeUp {
resp = append(resp, cty.ObjectVal(map[string]cty.Value{
"state": v,
"identity": ids[i],
"display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)),
}))
}
ret := map[string]cty.Value{
"data": cty.TupleVal(resp),
}
for k, v := range request.Config.AsValueMap() {
if k != "data" {
ret[k] = v
}
}
return providers.ListResourceResponse{Result: cty.ObjectVal(ret)}
},
assertChanges: func(sch providers.ProviderSchema, changes *plans.ChangesSrc) {
expectedResources := []string{"list.test_resource.test"}
actualResources := make([]string, 0)
for _, change := range changes.Queries {
actualResources = append(actualResources, change.Addr.String())
schema := sch.ListResourceTypes[change.Addr.Resource.Resource.Type]
cs, err := change.Decode(schema)
if err != nil {
t.Fatalf("failed to decode change: %s", err)
}
// Verify instance types
expectedTypes := []string{"ami-123456", "ami-654321"}
actualTypes := make([]string, 0)
obj := cs.Results.Value.GetAttr("data")
if obj.IsNull() {
t.Fatalf("Expected 'data' attribute to be present, but it is null")
}
obj.ForEachElement(func(key cty.Value, val cty.Value) bool {
val = val.GetAttr("state")
if val.IsNull() {
t.Fatalf("Expected 'state' attribute to be present, but it is null")
}
if val.GetAttr("instance_type").IsNull() {
t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing")
}
actualTypes = append(actualTypes, val.GetAttr("instance_type").AsString())
return false
})
sort.Strings(actualTypes)
sort.Strings(expectedTypes)
if diff := cmp.Diff(expectedTypes, actualTypes); diff != "" {
t.Fatalf("Expected instance types to match, but they differ: %s", diff)
}
}
sort.Strings(actualResources)
sort.Strings(expectedResources)
if diff := cmp.Diff(expectedResources, actualResources); diff != "" {
t.Fatalf("Expected resources to match, but they differ: %s", diff)
}
},
},
{
name: "invalid list result's attribute reference",
mainConfig: `
@ -301,10 +483,17 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
`,
diagCount: 1,
expectedErrMsg: []string{
"Invalid list resource traversal",
"The first step in the traversal for a list resource must be an attribute \"data\"",
assertValidateDiags: func(t *testing.T, diags tfdiags.Diagnostics) {
tfdiags.AssertDiagnosticCount(t, diags, 1)
var exp tfdiags.Diagnostics
exp = exp.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid list resource traversal",
Detail: "The first step in the traversal for a list resource must be an attribute \"data\".",
Subject: diags[0].Source().Subject.ToHCL().Ptr(),
})
tfdiags.AssertDiagnosticsMatch(t, diags, exp)
},
},
{
@ -332,9 +521,18 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
`,
diagCount: 1,
expectedErrMsg: []string{
"A list resource \"non_existent\" \"attr\" has not been declared in the root module.",
assertValidateDiags: func(t *testing.T, diags tfdiags.Diagnostics) {
tfdiags.AssertDiagnosticCount(t, diags, 1)
var exp tfdiags.Diagnostics
exp = exp.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared resource",
Detail: "A list resource \"non_existent\" \"attr\" has not been declared in the root module.",
Subject: diags[0].Source().Subject.ToHCL().Ptr(),
})
tfdiags.AssertDiagnosticsMatch(t, diags, exp)
},
},
{
@ -373,9 +571,18 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
`,
diagCount: 1,
expectedErrMsg: []string{
"Unsupported attribute: This object has no argument, nested block, or exported attribute named \"invalid_attr\".",
assertValidateDiags: func(t *testing.T, diags tfdiags.Diagnostics) {
tfdiags.AssertDiagnosticCount(t, diags, 1)
var exp tfdiags.Diagnostics
exp = exp.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsupported attribute",
Detail: "This object has no argument, nested block, or exported attribute named \"invalid_attr\".",
Subject: diags[0].Source().Subject.ToHCL().Ptr(),
})
tfdiags.AssertDiagnosticsMatch(t, diags, exp)
},
listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse {
madeUp := []cty.Value{
@ -444,9 +651,14 @@ func TestContext2Plan_queryList(t *testing.T) {
}
}
`,
diagCount: 1,
expectedErrMsg: []string{
"Cycle: list.test_resource",
assertValidateDiags: func(t *testing.T, diags tfdiags.Diagnostics) {
tfdiags.AssertDiagnosticCount(t, diags, 1)
if !strings.Contains(diags[0].Description().Summary, "Cycle: list.test_resource") {
t.Errorf("Expected error message to contain 'Cycle: list.test_resource', got %q", diags[0].Description().Summary)
}
if diags[0].Severity() != tfdiags.Error {
t.Errorf("Expected error severity to be Error, got %s", diags[0].Severity())
}
},
},
{
@ -564,8 +776,7 @@ func TestContext2Plan_queryList(t *testing.T) {
},
},
{
// Test list reference with index but without data field
name: "list reference with index but without data field",
name: "list reference as for_each",
mainConfig: `
terraform {
required_providers {
@ -688,8 +899,14 @@ func TestContext2Plan_queryList(t *testing.T) {
provider := testProvider("test")
provider.ConfigureProvider(providers.ConfigureProviderRequest{})
provider.GetProviderSchemaResponse = getListProviderSchemaResp()
if tc.transformSchema != nil {
tc.transformSchema(provider.GetProviderSchemaResponse)
}
var requestConfigs = make(map[string]cty.Value)
provider.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
if request.Config.IsNull() || request.Config.GetAttr("config").IsNull() {
t.Fatalf("config should never be null, got null for %s", request.TypeName)
}
requestConfigs[request.TypeName] = request.Config
fn := tc.listResourceFn
if fn == nil {
@ -705,15 +922,21 @@ func TestContext2Plan_queryList(t *testing.T) {
})
tfdiags.AssertNoDiagnostics(t, diags)
diags = ctx.Validate(mod, &ValidateOpts{})
if tc.assertValidateDiags != nil {
tc.assertValidateDiags(t, diags)
return
} else {
tfdiags.AssertNoDiagnostics(t, diags)
}
plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
SetVariables: testInputValuesUnset(mod.Module.Variables),
Query: true,
GenerateConfigPath: tc.generatedPath,
})
if len(diags) != tc.diagCount {
t.Fatalf("expected %d diagnostics, got %d \n -diags: %s", tc.diagCount, len(diags), diags)
}
tfdiags.AssertNoDiagnostics(t, diags)
if tc.assertChanges != nil {
sch, err := ctx.Schemas(mod, states.NewState())
@ -722,15 +945,6 @@ func TestContext2Plan_queryList(t *testing.T) {
}
tc.assertChanges(sch.Providers[providerAddr], plan.Changes)
}
if tc.diagCount > 0 {
for _, err := range tc.expectedErrMsg {
if !strings.Contains(diags.Err().Error(), err) {
t.Fatalf("expected error message %q, but got %q", err, diags.Err().Error())
}
}
}
})
}
}
@ -837,6 +1051,9 @@ func TestContext2Plan_queryListArgs(t *testing.T) {
provider.GetProviderSchemaResponse = getListProviderSchemaResp()
var recordedRequest providers.ListResourceRequest
provider.ListResourceFn = func(request providers.ListResourceRequest) providers.ListResourceResponse {
if request.Config.IsNull() || request.Config.GetAttr("config").IsNull() {
t.Fatalf("config should never be null, got null for %s", request.TypeName)
}
recordedRequest = request
return provider.ListResourceResponse
}

@ -303,7 +303,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid list resource traversal`,
Detail: fmt.Sprintf(`The first step in the traversal for a %s resource must be an attribute "data", but got %q instead.`, modeAdjective, remain[0]),
Detail: fmt.Sprintf(`The first step in the traversal for a %s resource must be an attribute "data".`, modeAdjective),
Subject: rng.ToHCL().Ptr(),
})
return diags
@ -315,7 +315,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid list resource traversal`,
Detail: fmt.Sprintf(`The second step in the traversal for a %s resource must be an index, but got %q instead.`, modeAdjective, remain[0]),
Detail: fmt.Sprintf(`The second step in the traversal for a %s resource must be an index.`, modeAdjective),
Subject: rng.ToHCL().Ptr(),
})
return diags
@ -331,7 +331,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid list resource traversal`,
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity", but got %q instead.`, modeAdjective, remain[0]),
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity".`, modeAdjective),
Subject: rng.ToHCL().Ptr(),
})
return diags
@ -340,7 +340,7 @@ func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid list resource traversal`,
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity", but got %q instead.`, modeAdjective, stateOrIdent.Name),
Detail: fmt.Sprintf(`The third step in the traversal for a %s resource must be an attribute "state" or "identity".`, modeAdjective),
Subject: rng.ToHCL().Ptr(),
})
return diags

@ -36,9 +36,15 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
keyData = EvalDataForInstanceKey(addr.Resource.Key, forEach)
}
schema := providerSchema.SchemaForListResourceType(n.Config.Type)
if schema.IsNil() { // Not possible, as the schema should have already been validated to exist
diags = diags.Append(fmt.Errorf("no schema available for %s; this is a bug in Terraform and should be reported", addr))
return diags
}
// evaluate the list config block
var configDiags tfdiags.Diagnostics
blockVal, _, configDiags := ctx.EvaluateBlock(config.Config, n.Schema.Body, nil, keyData)
blockVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.FullSchema, nil, keyData)
diags = diags.Append(configDiags)
if diags.HasErrors() {
return diags
@ -79,6 +85,13 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di
}
log.Printf("[TRACE] NodePlannableResourceInstance: Re-validating config for %s", n.Addr)
// if the config value is null, we still want to send a full object with all attributes being null
if !unmarkedBlockVal.IsNull() && unmarkedBlockVal.GetAttr("config").IsNull() {
mp := unmarkedBlockVal.AsValueMap()
mp["config"] = schema.ConfigSchema.EmptyValue()
unmarkedBlockVal = cty.ObjectVal(mp)
}
validateResp := provider.ValidateListResourceConfig(
providers.ValidateListResourceConfigRequest{
TypeName: n.Config.Type,

@ -471,8 +471,8 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
resp := provider.ValidateEphemeralResourceConfig(req)
diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()))
case addrs.ListResourceMode:
schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type)
if schema.Body == nil {
schema := providerSchema.SchemaForListResourceType(n.Config.Type)
if schema.IsNil() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid list resource",
@ -482,7 +482,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
return diags
}
blockVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData)
blockVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.FullSchema, nil, keyData)
diags = diags.Append(valDiags)
if valDiags.HasErrors() {
return diags
@ -502,6 +502,13 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
// Use unmarked value for validate request
unmarkedBlockVal, _ := blockVal.UnmarkDeep()
// if the config value is null, we still want to send a full object with all attributes being null
if !unmarkedBlockVal.IsNull() && unmarkedBlockVal.GetAttr("config").IsNull() {
mp := unmarkedBlockVal.AsValueMap()
mp["config"] = schema.ConfigSchema.EmptyValue()
unmarkedBlockVal = cty.ObjectVal(mp)
}
req := providers.ValidateListResourceConfigRequest{
TypeName: n.Config.Type,
Config: unmarkedBlockVal,

Loading…
Cancel
Save