diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index 704e2a03ce..ce214c80e9 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -161,7 +161,7 @@ func TestParserLoadConfigDirWithQueries(t *testing.T) { { name: "simple", directory: "testdata/query-files/valid/simple", - listResources: 2, + listResources: 3, allowExperiments: true, }, { diff --git a/internal/configs/query_file.go b/internal/configs/query_file.go index 6d06cb1beb..6820fee90f 100644 --- a/internal/configs/query_file.go +++ b/internal/configs/query_file.go @@ -150,6 +150,36 @@ func decodeQueryListBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { r.List.IncludeResource = attr.Expr } + // verify that the list block has a config block + content, contentDiags = block.Body.Content(&hcl.BodySchema{ + Attributes: QueryListResourceBlockSchema.Attributes, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "config"}, + }, + }) + diags = append(diags, contentDiags...) + + var configBlock hcl.Body + for _, block := range content.Blocks { + switch block.Type { + case "config": + if configBlock != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate config block", + Detail: "A list block must contain only one nested \"config\" block.", + Subject: block.DefRange.Ptr(), + }) + continue + } + configBlock = block.Body + default: + // Should not get here because the above should cover all + // block types declared in the schema. + panic(fmt.Sprintf("unhandled block type %q", block.Type)) + } + } + return &r, diags } diff --git a/internal/configs/testdata/config-diagnostics/list-in-child-module/child/main.tfquery.hcl b/internal/configs/testdata/config-diagnostics/list-in-child-module/child/main.tfquery.hcl index 964271e182..0108c2ab30 100644 --- a/internal/configs/testdata/config-diagnostics/list-in-child-module/child/main.tfquery.hcl +++ b/internal/configs/testdata/config-diagnostics/list-in-child-module/child/main.tfquery.hcl @@ -1,7 +1,10 @@ list "test_resource" "test" { provider = azurerm count = 1 - tags = { - Name = "test" + + config { + tags = { + Name = "test" + } } } \ No newline at end of file diff --git a/internal/configs/testdata/query-files/invalid/no-provider/main.tfquery.hcl b/internal/configs/testdata/query-files/invalid/no-provider/main.tfquery.hcl index 2be3925551..d035fef672 100644 --- a/internal/configs/testdata/query-files/invalid/no-provider/main.tfquery.hcl +++ b/internal/configs/testdata/query-files/invalid/no-provider/main.tfquery.hcl @@ -1,6 +1,8 @@ list "aws_instance" "test" { count = 1 - tags = { - Name = "test" + config { + tags = { + Name = "test" + } } } \ No newline at end of file diff --git a/internal/configs/testdata/query-files/valid/mixed/main.tfquery.hcl b/internal/configs/testdata/query-files/valid/mixed/main.tfquery.hcl index 35535be838..e1cef3b344 100644 --- a/internal/configs/testdata/query-files/valid/mixed/main.tfquery.hcl +++ b/internal/configs/testdata/query-files/valid/mixed/main.tfquery.hcl @@ -1,14 +1,18 @@ list "aws_instance" "test" { provider = aws count = 1 - tags = { - Name = "test" + config { + tags = { + Name = "test" + } } } list "aws_instance" "test2" { provider = aws count = 1 - tags = { - Name = join("-", ["test2", list.aws_instance.test.data[0]]) + config { + tags = { + Name = join("-", ["test2", list.aws_instance.test.data[0]]) + } } } \ No newline at end of file diff --git a/internal/configs/testdata/query-files/valid/simple/main.tfquery.hcl b/internal/configs/testdata/query-files/valid/simple/main.tfquery.hcl index d331c29967..dff10a6287 100644 --- a/internal/configs/testdata/query-files/valid/simple/main.tfquery.hcl +++ b/internal/configs/testdata/query-files/valid/simple/main.tfquery.hcl @@ -2,14 +2,21 @@ list "aws_instance" "test" { provider = aws count = 1 include_resource = true - tags = { - Name = "test" + config { + tags = { + Name = "test" + } } } list "aws_instance" "test2" { provider = aws count = 1 - tags = { - Name = join("-", ["test2", list.aws_instance.test.data[0]]) + config { + tags = { + Name = join("-", ["test2", list.aws_instance.test.data[0]]) + } } -} \ No newline at end of file +} +list "aws_instance" "test3" { + provider = aws +} diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index bd1010429b..e3b31264e5 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -376,7 +376,11 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour } configSchema := listResourceSchema.Body.BlockTypes["config"] - mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType()) + config := cty.NullVal(configSchema.ImpliedType()) + if r.Config.Type().HasAttribute("config") { + config = r.Config.GetAttr("config") + } + mp, err := msgpack.Marshal(config, configSchema.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp diff --git a/internal/plugin/grpc_provider_test.go b/internal/plugin/grpc_provider_test.go index 74477609e7..de819c1d12 100644 --- a/internal/plugin/grpc_provider_test.go +++ b/internal/plugin/grpc_provider_test.go @@ -451,7 +451,43 @@ func TestGRPCProvider_ValidateListResourceConfig(t *testing.T) { gomock.Any(), ).Return(&proto.ValidateListResourceConfig_Response{}, nil) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"filter_attr": "value"}) + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value"}}) + resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{ + TypeName: "list", + Config: cfg, + }) + checkDiags(t, resp.Diagnostics) +} + +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 + // we always need a GetSchema method + client.EXPECT().GetSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(sch, nil) + + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + client.EXPECT().ValidateListResourceConfig( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ValidateListResourceConfig_Response{}, nil) + + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{}) resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{ TypeName: "list", Config: cfg, diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 21b63699a3..0073ccbfec 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -373,7 +373,11 @@ func (p *GRPCProvider) ValidateListResourceConfig(r providers.ValidateListResour return resp } configSchema := listResourceSchema.Body.BlockTypes["config"] - mp, err := msgpack.Marshal(r.Config, configSchema.ImpliedType()) + config := cty.NullVal(configSchema.ImpliedType()) + if r.Config.Type().HasAttribute("config") { + config = r.Config.GetAttr("config") + } + mp, err := msgpack.Marshal(config, configSchema.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp diff --git a/internal/plugin6/grpc_provider_test.go b/internal/plugin6/grpc_provider_test.go index fe34b59515..eb5935f6a3 100644 --- a/internal/plugin6/grpc_provider_test.go +++ b/internal/plugin6/grpc_provider_test.go @@ -458,7 +458,43 @@ func TestGRPCProvider_ValidateListResourceConfig(t *testing.T) { gomock.Any(), ).Return(&proto.ValidateListResourceConfig_Response{}, nil) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"filter_attr": "value"}) + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{"config": map[string]interface{}{"filter_attr": "value"}}) + resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{ + TypeName: "list", + Config: cfg, + }) + checkDiags(t, resp.Diagnostics) +} + +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 + // we always need a GetSchema method + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(sch, nil) + + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + client.EXPECT().ValidateListResourceConfig( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ValidateListResourceConfig_Response{}, nil) + + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{}) resp := p.ValidateListResourceConfig(providers.ValidateListResourceConfigRequest{ TypeName: "list", Config: cfg, diff --git a/internal/terraform/context_plan_query_test.go b/internal/terraform/context_plan_query_test.go index 714f003ba9..eaf5917eeb 100644 --- a/internal/terraform/context_plan_query_test.go +++ b/internal/terraform/context_plan_query_test.go @@ -104,16 +104,20 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test.data[0].state.instance_type + config { + filter = { + attr = list.test_resource.test.data[0].state.instance_type + } } } `, @@ -218,16 +222,20 @@ func TestContext2Plan_queryList(t *testing.T) { count = 1 provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test[0].data[0].state.instance_type + config { + filter = { + attr = list.test_resource.test[0].data[0].state.instance_type + } } } `, @@ -330,16 +338,20 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test.state.instance_type + config { + filter = { + attr = list.test_resource.test.state.instance_type + } } } `, @@ -366,8 +378,10 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = list.non_existent.attr + config { + filter = { + attr = list.non_existent.attr + } } } `, @@ -393,16 +407,20 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = "valid" + config { + filter = { + attr = "valid" + } } } list "test_resource" "another" { provider = test - filter = { - attr = list.test_resource.test.data[0].state.invalid_attr + config { + filter = { + attr = list.test_resource.test.data[0].state.invalid_attr + } } } `, @@ -458,16 +476,20 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test1" { provider = test - filter = { - attr = list.test_resource.test2.data[0].state.id + config { + filter = { + attr = list.test_resource.test2.data[0].state.id + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test1.data[0].state.id + config { + filter = { + attr = list.test_resource.test1.data[0].state.id + } } } `, @@ -498,16 +520,20 @@ func TestContext2Plan_queryList(t *testing.T) { list "test_resource" "test1" { provider = test - filter = { - attr = var.test_var + config { + filter = { + attr = var.test_var + } } } list "test_resource" "test2" { provider = test - filter = { - attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var + config { + filter = { + attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var + } } } `, @@ -606,8 +632,10 @@ func TestContext2Plan_queryList(t *testing.T) { for_each = toset(["foo", "bar"]) provider = test - filter = { - attr = each.value + config { + filter = { + attr = each.value + } } } @@ -615,8 +643,10 @@ func TestContext2Plan_queryList(t *testing.T) { provider = test for_each = list.test_resource.test1 - filter = { - attr = each.value.data[0].state.instance_type + config { + filter = { + attr = each.value.data[0].state.instance_type + } } } `, diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 1045ec59a9..cf1124147a 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -3105,6 +3105,29 @@ func TestContext2Validate_queryList(t *testing.T) { diagCount int expectedErrMsg []string }{ + { + name: "valid simple block", + 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 + } + `, + }, { name: "valid list reference", mainConfig: ` @@ -3126,16 +3149,20 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test.data[0].state.instance_type + config { + filter = { + attr = list.test_resource.test.data[0].state.instance_type + } } } `, @@ -3162,16 +3189,20 @@ func TestContext2Validate_queryList(t *testing.T) { count = 1 provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test[0].data[0].state.instance_type + config { + filter = { + attr = list.test_resource.test[0].data[0].state.instance_type + } } } `, @@ -3197,16 +3228,20 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test.state.instance_type + config { + filter = { + attr = list.test_resource.test.state.instance_type + } } } `, @@ -3248,8 +3283,10 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = var.input + config { + filter = { + attr = var.input + } } } `, @@ -3288,8 +3325,10 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = resource.list.test_resource.attr + config { + filter = { + attr = resource.list.test_resource.attr + } } } `, @@ -3311,8 +3350,10 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = list.non_existent.attr + config { + filter = { + attr = list.non_existent.attr + } } } `, @@ -3338,16 +3379,20 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test" { provider = test - filter = { - attr = "valid" + config { + filter = { + attr = "valid" + } } } list "test_resource" "another" { provider = test - filter = { - attr = list.test_resource.test.data[0].state.invalid_attr + config { + filter = { + attr = list.test_resource.test.data[0].state.invalid_attr + } } } `, @@ -3372,16 +3417,20 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test1" { provider = test - filter = { - attr = list.test_resource.test2.data[0].state.id + config { + filter = { + attr = list.test_resource.test2.data[0].state.id + } } } list "test_resource" "test2" { provider = test - filter = { - attr = list.test_resource.test1.data[0].state.id + config { + filter = { + attr = list.test_resource.test1.data[0].state.id + } } } `, @@ -3412,16 +3461,20 @@ func TestContext2Validate_queryList(t *testing.T) { list "test_resource" "test1" { provider = test - filter = { - attr = var.test_var + config { + filter = { + attr = var.test_var + } } } list "test_resource" "test2" { provider = test - filter = { - attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var + config { + filter = { + attr = length(list.test_resource.test1.data) > 0 ? list.test_resource.test1.data[0].state.instance_type : var.test_var + } } } `, @@ -3444,8 +3497,10 @@ func TestContext2Validate_queryList(t *testing.T) { for_each = toset(["foo", "bar"]) provider = test - filter = { - attr = each.value + config { + filter = { + attr = each.value + } } } @@ -3453,8 +3508,10 @@ func TestContext2Validate_queryList(t *testing.T) { provider = test for_each = list.test_resource.test1 - filter = { - attr = each.value.data[0].instance_type + config { + filter = { + attr = each.value.data[0].instance_type + } } } `, @@ -3480,6 +3537,7 @@ func TestContext2Validate_queryList(t *testing.T) { PreloadedProviderSchemas: map[addrs.Provider]providers.ProviderSchema{ providerAddr: *provider.GetProviderSchemaResponse, }, + Parallelism: 1, }) tfdiags.AssertNoDiagnostics(t, diags) @@ -3540,24 +3598,34 @@ func getTestProvider() *testing_provider.MockProvider { // getQueryTestSchema returns a schema for query tests with a filter attribute func getQueryTestSchema() *configschema.Block { - return &configschema.Block{ + body := &configschema.Block{ Attributes: map[string]*configschema.Attribute{ - "filter": { - Required: true, - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, + "data": { + Type: cty.DynamicPseudoType, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "config": { + Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ - "attr": { - Type: cty.String, + "filter": { Required: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Required: true, + }, + }, + }, }, }, }, - }, - "data": { - Computed: true, - Type: cty.DynamicPseudoType, + Nesting: configschema.NestingSingle, }, }, } + return body } diff --git a/internal/terraform/node_resource_plan_instance_query.go b/internal/terraform/node_resource_plan_instance_query.go index 610758f54a..7ede8d6329 100644 --- a/internal/terraform/node_resource_plan_instance_query.go +++ b/internal/terraform/node_resource_plan_instance_query.go @@ -38,15 +38,15 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di // evaluate the list config block var configDiags tfdiags.Diagnostics - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, n.Schema.Body, nil, keyData) + blockVal, _, configDiags := ctx.EvaluateBlock(config.Config, n.Schema.Body, nil, keyData) diags = diags.Append(configDiags) if diags.HasErrors() { return diags } // Unmark before sending to provider - unmarkedConfigVal, _ := configVal.UnmarkDeepWithPaths() - configKnown := configVal.IsWhollyKnown() + unmarkedBlockVal, _ := blockVal.UnmarkDeepWithPaths() + configKnown := blockVal.IsWhollyKnown() if !configKnown { diags = diags.Append(fmt.Errorf("config is not known")) return diags @@ -56,7 +56,7 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di validateResp := provider.ValidateListResourceConfig( providers.ValidateListResourceConfigRequest{ TypeName: n.Config.Type, - Config: unmarkedConfigVal, + Config: unmarkedBlockVal, }, ) diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) @@ -68,7 +68,7 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di // to actually call the provider to list the data. resp := provider.ListResource(providers.ListResourceRequest{ TypeName: n.Config.Type, - Config: unmarkedConfigVal, + Config: unmarkedBlockVal, }) if resp.Diagnostics != nil { return diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index e928d73074..06f1e7a719 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -482,16 +482,16 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) + blockVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags } // Use unmarked value for validate request - unmarkedConfigVal, _ := configVal.UnmarkDeep() + unmarkedBlockVal, _ := blockVal.UnmarkDeep() req := providers.ValidateListResourceConfigRequest{ TypeName: n.Config.Type, - Config: unmarkedConfigVal, + Config: unmarkedBlockVal, } resp := provider.ValidateListResourceConfig(req)