diff --git a/internal/command/jsonlist/list.go b/internal/command/jsonlist/list.go index 7f65e38d34b6..38e97362afee 100644 --- a/internal/command/jsonlist/list.go +++ b/internal/command/jsonlist/list.go @@ -26,8 +26,11 @@ type QueryResult struct { Resource map[string]json.RawMessage `json:"resource,omitempty"` // TODO - // Address string `json:"address,omitempty"` + Address string `json:"address,omitempty"` // Config string `json:"config,omitempty"` + + ResourceConfig string `json:"resource_config,omitempty"` + ImportConfig string `json:"import_config,omitempty"` } func MarshalForRenderer( @@ -79,6 +82,9 @@ func marshalQueryInstance(rc *plans.QueryInstanceSrc, schemas *terraform.Schemas Identity: marshalValues(value.GetAttr("identity")), Resource: marshalValues(value.GetAttr("state")), } + config := query.Results.Generated.Results[result.Address] + result.ResourceConfig = string(config.Body) + result.ImportConfig = string(config.Import) ret.Results = append(ret.Results, result) } diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go index 1faf9939e722..b94189623d7e 100644 --- a/internal/genconfig/generate_config.go +++ b/internal/genconfig/generate_config.go @@ -4,6 +4,7 @@ package genconfig import ( + "bytes" "encoding/json" "fmt" "maps" @@ -21,6 +22,51 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) +type Resource struct { + // HCL Body of the resource, which is the attributes and blocks + // that are part of the resource. + Body []byte + + // Import is the HCL code for the import block. This is only + // generated for list resource results. + Import []byte + Addr addrs.AbsResourceInstance + Results map[string]*Resource +} + +func (r *Resource) String() string { + var buf strings.Builder + switch r.Addr.Resource.Resource.Mode { + case addrs.ListResourceMode: + last := len(r.Results) - 1 + // sort the results by their keys so the output is consistent + for idx, key := range slices.Sorted(maps.Keys(r.Results)) { + managed := r.Results[key] + if managed.Body != nil { + buf.WriteString(managed.String()) + buf.WriteString("\n") + } + if managed.Import != nil { + buf.WriteString(string(managed.Import)) + buf.WriteString("\n") + } + if idx != last { + buf.WriteString("\n") + } + } + case addrs.ManagedResourceMode: + buf.WriteString(fmt.Sprintf("resource %q %q {\n", r.Addr.Resource.Resource.Type, r.Addr.Resource.Resource.Name)) + buf.Write(r.Body) + buf.WriteString("}") + default: + panic(fmt.Errorf("unsupported resource mode %s", r.Addr.Resource.Resource.Mode)) + } + + // The output better be valid HCL which can be parsed and formatted. + formatted := hclwrite.Format([]byte(buf.String())) + return string(formatted) +} + // GenerateResourceContents generates HCL configuration code for the provided // resource and state value. // @@ -30,7 +76,7 @@ import ( func GenerateResourceContents(addr addrs.AbsResourceInstance, schema *configschema.Block, pc addrs.LocalProviderConfig, - stateVal cty.Value) (string, tfdiags.Diagnostics) { + stateVal cty.Value) (*Resource, tfdiags.Diagnostics) { var buf strings.Builder var diags tfdiags.Diagnostics @@ -44,25 +90,101 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance, diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2)) diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2)) } else { - diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2, optionalOrRequiredProcessor)) diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2)) } // The output better be valid HCL which can be parsed and formatted. formatted := hclwrite.Format([]byte(buf.String())) - return string(formatted), diags + return &Resource{ + Body: formatted, + Addr: addr, + }, diags } -func WrapResourceContents(addr addrs.AbsResourceInstance, config string) string { +func GenerateListResourceContents(addr addrs.AbsResourceInstance, + schema *configschema.Block, + idSchema *configschema.Object, + pc addrs.LocalProviderConfig, + stateVal cty.Value, +) (*Resource, tfdiags.Diagnostics) { + hclFmt := func(s []byte) []byte { + return bytes.TrimSpace(hclwrite.Format(s)) + } + ret := make(map[string]*Resource) + var diags tfdiags.Diagnostics + if !stateVal.CanIterateElements() { + diags = diags.Append( + hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid resource instance value", + Detail: fmt.Sprintf("Resource instance %s has nil or non-iterable value", addr), + }) + return nil, diags + } + + iter := stateVal.ElementIterator() + for idx := 0; iter.Next(); idx++ { + // Generate a unique resource name for each instance in the list. + resAddr := addrs.AbsResourceInstance{ + Module: addr.Module, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: addr.Resource.Resource.Type, + Name: fmt.Sprintf("%s_%d", addr.Resource.Resource.Name, idx), + }, + Key: addr.Resource.Key, + }, + } + ls := &Resource{Addr: resAddr} + + _, val := iter.Element() + // we still need to generate the resource block even if the state is not given, + // so that the import block can reference it. + stateVal := cty.NilVal + if val.Type().HasAttribute("state") { + stateVal = val.GetAttr("state") + } + content, gDiags := GenerateResourceContents(resAddr, schema, pc, stateVal) + if gDiags.HasErrors() { + diags = diags.Append(gDiags) + continue + } + ls.Body = content.Body + + idVal := val.GetAttr("identity") + importContent, gDiags := generateImportBlock(resAddr, idSchema, pc, idVal) + if gDiags.HasErrors() { + diags = diags.Append(gDiags) + continue + } + ls.Import = hclFmt([]byte(importContent)) + + ret[resAddr.String()] = ls + } + + return &Resource{ + Results: ret, + Addr: addr, + }, diags +} + +func generateImportBlock(addr addrs.AbsResourceInstance, idSchema *configschema.Object, pc addrs.LocalProviderConfig, identity cty.Value) (string, tfdiags.Diagnostics) { var buf strings.Builder + var diags tfdiags.Diagnostics - buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name)) - buf.WriteString(config) - buf.WriteString("}") + buf.WriteString("\n") + buf.WriteString("import {\n") + buf.WriteString(fmt.Sprintf(" to = %s\n", addr.String())) + buf.WriteString(fmt.Sprintf(" provider = %s\n", pc.StringCompact())) + buf.WriteString(" identity = {\n") + diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, identity, idSchema.Attributes, 2, allowAllAttributesProcessor)) + buf.WriteString(strings.Repeat(" ", 2)) + buf.WriteString("}\n}\n") - // The output better be valid HCL which can be parsed and formatted. formatted := hclwrite.Format([]byte(buf.String())) - return string(formatted) + return string(formatted), diags } func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { @@ -112,7 +234,16 @@ func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, return diags } -func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { +func optionalOrRequiredProcessor(attr *configschema.Attribute) bool { + // Exclude computed-only attributes + return attr.Optional || attr.Required +} + +func allowAllAttributesProcessor(attr *configschema.Attribute) bool { + return true +} + +func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int, processAttr func(*configschema.Attribute) bool) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if len(attrs) == 0 { return diags @@ -126,8 +257,7 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri continue } - // Exclude computed-only attributes - if attrS.Required || attrS.Optional { + if processAttr != nil && processAttr(attrS) { buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(fmt.Sprintf("%s = ", name)) @@ -327,6 +457,7 @@ func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings. func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics { var diags tfdiags.Diagnostics + processor := optionalOrRequiredProcessor switch schema.NestedType.Nesting { case configschema.NestingSingle: @@ -354,7 +485,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(fmt.Sprintf("%s = {\n", name)) - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2, processor)) buf.WriteString("}\n") return diags @@ -386,7 +517,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, } buf.WriteString("{\n") - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4, processor)) buf.WriteString(strings.Repeat(" ", indent+2)) buf.WriteString("},\n") } @@ -424,7 +555,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, } buf.WriteString("\n") - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4, processor)) buf.WriteString(strings.Repeat(" ", indent+2)) buf.WriteString("}\n") } @@ -440,6 +571,7 @@ func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics { var diags tfdiags.Diagnostics + processAttr := optionalOrRequiredProcessor switch schema.Nesting { case configschema.NestingSingle, configschema.NestingGroup: @@ -455,7 +587,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str return diags } buf.WriteString("\n") - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2, processAttr)) diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, stateVal, schema.BlockTypes, indent+2)) buf.WriteString("}\n") return diags @@ -469,7 +601,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str for i := range listVals { buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString(fmt.Sprintf("%s {\n", name)) - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2, processAttr)) diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2)) buf.WriteString("}\n") } @@ -491,7 +623,7 @@ func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *str return diags } buf.WriteString("\n") - diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2, processAttr)) diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, vals[key], schema.BlockTypes, indent+2)) buf.WriteString(strings.Repeat(" ", indent)) buf.WriteString("}\n") diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index 87d3fb778f85..a8593d5670b7 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -4,6 +4,8 @@ package genconfig import ( + "maps" + "slices" "strings" "testing" @@ -825,12 +827,12 @@ resource "tfcoremock_sensitive_values" "values" { if err != nil { t.Fatalf("schema failed InternalValidate: %s", err) } - contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, tc.value) + content, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, tc.value) if len(diags) > 0 { t.Errorf("expected no diagnostics but found %s", diags) } - got := WrapResourceContents(tc.addr, contents) + got := content.String() want := strings.TrimSpace(tc.expected) if diff := cmp.Diff(got, want); len(diff) > 0 { t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) @@ -846,3 +848,179 @@ func sensitiveAttribute(t cty.Type) *configschema.Attribute { Sensitive: true, } } + +func TestGenerateResourceAndIDContents(t *testing.T) { + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Computed: true, + }, + "tags": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "network_interface": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "subnet_id": { + Type: cty.String, + Required: true, + }, + "ip_address": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + // Define the identity schema + idSchema := &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + } + + // Create mock resource instance values + value := cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "state": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("instance-1"), + "id": cty.StringVal("i-abcdef"), + "tags": cty.MapVal(map[string]cty.Value{ + "Environment": cty.StringVal("Dev"), + "Owner": cty.StringVal("Team1"), + }), + "network_interface": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "subnet_id": cty.StringVal("subnet-123"), + "ip_address": cty.StringVal("10.0.0.1"), + }), + }), + }), + "identity": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abcdef"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "state": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("instance-2"), + "id": cty.StringVal("i-123456"), + "tags": cty.MapVal(map[string]cty.Value{ + "Environment": cty.StringVal("Prod"), + "Owner": cty.StringVal("Team2"), + }), + "network_interface": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "subnet_id": cty.StringVal("subnet-456"), + "ip_address": cty.StringVal("10.0.0.2"), + }), + }), + }), + "identity": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-123456"), + }), + }), + }) + + // Create test resource address + addr := addrs.AbsResource{ + Module: addrs.RootModuleInstance, + Resource: addrs.Resource{ + Mode: addrs.ListResourceMode, + Type: "aws_instance", + Name: "example", + }, + } + + // Create instance addresses for each instance + instAddr1 := addr.Instance(addrs.NoKey) + + // Create provider config + pc := addrs.LocalProviderConfig{ + LocalName: "aws", + } + + // Generate content + content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, value) + // Check for diagnostics + if diags.HasErrors() { + t.Fatalf("unexpected diagnostics: %s", diags.Err()) + } + + // Check the generated content + expectedContent := `resource "aws_instance" "example_0" { + name = "instance-1" + tags = { + Environment = "Dev" + Owner = "Team1" + } + network_interface { + ip_address = "10.0.0.1" + subnet_id = "subnet-123" + } +} +import { + to = aws_instance.example_0 + provider = aws + identity = { + id = "i-abcdef" + } +} + +resource "aws_instance" "example_1" { + name = "instance-2" + tags = { + Environment = "Prod" + Owner = "Team2" + } + network_interface { + ip_address = "10.0.0.2" + subnet_id = "subnet-456" + } +} +import { + to = aws_instance.example_1 + provider = aws + identity = { + id = "i-123456" + } +} +` + // Normalize both strings by removing extra whitespace for comparison + normalizeString := func(s string) string { + // Remove spaces at the end of lines and replace multiple newlines with a single one + lines := strings.Split(s, "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, " \t") + } + return strings.Join(lines, "\n") + } + + normalizedExpected := normalizeString(expectedContent) + + var merged string + res := content.Results + for _, addr := range slices.Sorted(maps.Keys(res)) { + merged += res[addr].String() + } + normalizedActual := normalizeString(content.String()) + + if diff := cmp.Diff(normalizedExpected, normalizedActual); diff != "" { + t.Errorf("Generated content doesn't match expected. want:\n%s\ngot:\n%s\ndiff:\n%s", normalizedExpected, normalizedActual, diff) + } +} diff --git a/internal/plans/changes.go b/internal/plans/changes.go index e2cb7dd1879c..5ed41122ace0 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -9,6 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/schemarepo" @@ -251,7 +252,8 @@ type QueryInstance struct { } type QueryResults struct { - Value cty.Value + Value cty.Value + Generated *genconfig.Resource } func (qi *QueryInstance) DeepCopy() *QueryInstance { @@ -273,6 +275,7 @@ func (rc *QueryInstance) Encode(schema providers.Schema) (*QueryInstanceSrc, err Addr: rc.Addr, Results: results, ProviderAddr: rc.ProviderAddr, + Generated: rc.Results.Generated, }, nil } diff --git a/internal/plans/changes_src.go b/internal/plans/changes_src.go index 023dca14ec28..1c62e6f30ab0 100644 --- a/internal/plans/changes_src.go +++ b/internal/plans/changes_src.go @@ -9,6 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/schemarepo" @@ -180,7 +181,8 @@ type QueryInstanceSrc struct { ProviderAddr addrs.AbsProviderConfig - Results DynamicValue + Results DynamicValue + Generated *genconfig.Resource } func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, error) { @@ -192,7 +194,8 @@ func (qis *QueryInstanceSrc) Decode(schema providers.Schema) (*QueryInstance, er return &QueryInstance{ Addr: qis.Addr, Results: QueryResults{ - Value: query, + Value: query, + Generated: qis.Generated, }, ProviderAddr: qis.ProviderAddr, }, nil diff --git a/internal/terraform/context_plan_query_test.go b/internal/terraform/context_plan_query_test.go index 5d6fc6f23771..ecbcc745acb2 100644 --- a/internal/terraform/context_plan_query_test.go +++ b/internal/terraform/context_plan_query_test.go @@ -5,6 +5,7 @@ package terraform import ( "fmt" + "maps" "sort" "strings" "testing" @@ -22,10 +23,12 @@ 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) @@ -33,7 +36,7 @@ func TestContext2Plan_queryList(t *testing.T) { listResourceFn func(request providers.ListResourceRequest) providers.ListResourceResponse }{ { - name: "valid list reference", + name: "valid list reference - generates config", mainConfig: ` terraform { required_providers { @@ -71,6 +74,7 @@ func TestContext2Plan_queryList(t *testing.T) { } } `, + generatedPath: t.TempDir(), listResourceFn: func(request providers.ListResourceRequest) providers.ListResourceResponse { madeUp := []cty.Value{ cty.ObjectVal(map[string]cty.Value{"instance_type": cty.StringVal("ami-123456")}), @@ -85,68 +89,62 @@ func TestContext2Plan_queryList(t *testing.T) { } resp := []cty.Value{} - if request.IncludeResourceObject { - 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)), - })) + for i, v := range madeUp { + mp := map[string]cty.Value{ + "identity": ids[i], + "display_name": cty.StringVal(fmt.Sprintf("Instance %d", i+1)), + } + if request.IncludeResourceObject { + mp["state"] = v } + resp = append(resp, cty.ObjectVal(mp)) } - ret := map[string]cty.Value{ + ret := request.Config.AsValueMap() + maps.Copy(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 := map[string][]string{ - "list.test_resource.test": {"ami-123456", "ami-654321", "ami-789012"}, - "list.test_resource.test2": {}, - } - actualResources := map[string][]string{} + expectedResources := []string{"list.test_resource.test", "list.test_resource.test2"} + actualResources := make([]string, 0) + generatedCfgs := 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 - 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 { - if !val.Type().HasAttribute("state") { - t.Fatalf("Expected 'state' attribute to be present, but it is missing") - } - - val = val.GetAttr("state") - if !val.IsNull() { - if val.GetAttr("instance_type").IsNull() { - t.Fatalf("Expected 'instance_type' attribute to be present, but it is missing") + if val.Type().HasAttribute("state") { + val = val.GetAttr("state") + if !val.IsNull() { + 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) - actualResources[change.Addr.String()] = actualTypes + generatedCfgs = append(generatedCfgs, change.Generated.String()) } if diff := cmp.Diff(expectedResources, actualResources); diff != "" { t.Fatalf("Expected resources to match, but they differ: %s", diff) } + + if diff := cmp.Diff([]string{testResourceCfg, testResourceCfg2}, generatedCfgs); diff != "" { + t.Fatalf("Expected generated configs to match, but they differ: %s", diff) + } }, }, { @@ -709,9 +707,10 @@ func TestContext2Plan_queryList(t *testing.T) { tfdiags.AssertNoDiagnostics(t, diags) plan, diags := ctx.Plan(mod, states.NewState(), &PlanOpts{ - Mode: plans.NormalMode, - SetVariables: testInputValuesUnset(mod.Module.Variables), - Query: true, + 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) @@ -924,6 +923,7 @@ func getListProviderSchemaResp() *providers.GetProviderSchemaResponse { "instance_type": { Type: cty.String, Computed: true, + Optional: true, }, }, }, @@ -962,3 +962,73 @@ func getListProviderSchemaResp() *providers.GetProviderSchemaResponse { }, }) } + +var ( + testResourceCfg = `resource "test_resource" "test_0" { + instance_type = "ami-123456" +} +import { + to = test_resource.test_0 + provider = test + identity = { + id = "i-v1" + } +} + +resource "test_resource" "test_1" { + instance_type = "ami-654321" +} +import { + to = test_resource.test_1 + provider = test + identity = { + id = "i-v2" + } +} + +resource "test_resource" "test_2" { + instance_type = "ami-789012" +} +import { + to = test_resource.test_2 + provider = test + identity = { + id = "i-v3" + } +} +` + + testResourceCfg2 = `resource "test_resource" "test2_0" { + instance_type = null # OPTIONAL string +} +import { + to = test_resource.test2_0 + provider = test + identity = { + id = "i-v1" + } +} + +resource "test_resource" "test2_1" { + instance_type = null # OPTIONAL string +} +import { + to = test_resource.test2_1 + provider = test + identity = { + id = "i-v2" + } +} + +resource "test_resource" "test2_2" { + instance_type = null # OPTIONAL string +} +import { + to = test_resource.test2_2 + provider = test + identity = { + id = "i-v3" + } +} +` +) diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index b1a4b4626ae3..511b2558f9ba 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -46,7 +46,7 @@ type NodeAbstractResourceInstance struct { preDestroyRefresh bool - // During import we may generate configuration for a resource, which needs + // During import (or query) we may generate configuration for a resource, which needs // to be stored in the final change. generatedConfigHCL string diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 2f38df5fa133..930d87ae39f9 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -839,16 +839,15 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. } // Generate the HCL string first, then parse the HCL body from it. - // First we generate the contents of the resource block for use within - // the planning node. Then we wrap it in an enclosing resource block to - // pass into the plan for rendering. - generatedHCLAttributes, generatedDiags := n.generateHCLStringAttributes(n.Addr, instanceRefreshState, schema.Body) + generatedResource, generatedDiags := n.generateHCLResourceDef(n.Addr, instanceRefreshState.Value, schema) diags = diags.Append(generatedDiags) - n.generatedConfigHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes) + // This wraps the content of the resource block in an enclosing resource block + // to pass into the plan for rendering. + n.generatedConfigHCL = generatedResource.String() - // parse the "file" as HCL to get the hcl.Body - synthHCLFile, hclDiags := hclsyntax.ParseConfig([]byte(generatedHCLAttributes), filepath.Base(n.generateConfigPath), hcl.Pos{Byte: 0, Line: 1, Column: 1}) + // parse the "file" body as HCL to get the hcl.Body + synthHCLFile, hclDiags := hclsyntax.ParseConfig(generatedResource.Body, filepath.Base(n.generateConfigPath), hcl.Pos{Byte: 0, Line: 1, Column: 1}) diags = diags.Append(hclDiags) if hclDiags.HasErrors() { return instanceRefreshState, nil, diags @@ -883,10 +882,11 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. return instanceRefreshState, deferred, diags } -// generateHCLStringAttributes produces a string in HCL format for the given -// resource state and schema without the surrounding block. -func (n *NodePlannableResourceInstance) generateHCLStringAttributes(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, schema *configschema.Block) (string, tfdiags.Diagnostics) { - filteredSchema := schema.Filter( +// generateHCLResourceDef generates the HCL definition for the resource +// instance, including the surrounding block. This is used to generate the +// configuration for the resource instance when importing or generating +func (n *NodePlannableResourceInstance) generateHCLResourceDef(addr addrs.AbsResourceInstance, state cty.Value, schema providers.Schema) (*genconfig.Resource, tfdiags.Diagnostics) { + filteredSchema := schema.Body.Filter( configschema.FilterOr( configschema.FilterReadOnlyAttribute, configschema.FilterDeprecatedAttribute, @@ -911,7 +911,15 @@ func (n *NodePlannableResourceInstance) generateHCLStringAttributes(addr addrs.A Alias: n.ResolvedProvider.Alias, } - return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state.Value) + switch addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state) + case addrs.ListResourceMode: + identitySchema := schema.Identity + return genconfig.GenerateListResourceContents(addr, filteredSchema, identitySchema, providerAddr, state) + default: + panic(fmt.Sprintf("unexpected resource mode %s for resource %s", addr.Resource.Resource.Mode, addr)) + } } // mergeDeps returns the union of 2 sets of dependencies diff --git a/internal/terraform/node_resource_plan_instance_query.go b/internal/terraform/node_resource_plan_instance_query.go index ffd1095eefe8..39c6786c2c23 100644 --- a/internal/terraform/node_resource_plan_instance_query.go +++ b/internal/terraform/node_resource_plan_instance_query.go @@ -7,6 +7,7 @@ import ( "fmt" "log" + "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" @@ -87,11 +88,23 @@ func (n *NodePlannableResourceInstance) listResourceExecute(ctx EvalContext) (di return diags } + // If a path is specified, generate the config for the resource + var generated *genconfig.Resource + if n.generateConfigPath != "" { + var gDiags tfdiags.Diagnostics + generated, gDiags = n.generateHCLResourceDef(addr, resp.Result.GetAttr("data"), providerSchema.ResourceTypes[n.Config.Type]) + diags = diags.Append(gDiags) + if diags.HasErrors() { + return diags + } + } + query := &plans.QueryInstance{ Addr: n.Addr, ProviderAddr: n.ResolvedProvider, Results: plans.QueryResults{ - Value: resp.Result, + Value: resp.Result, + Generated: generated, }, } diff --git a/internal/terraform/transform_config.go b/internal/terraform/transform_config.go index 735cc0ecf14f..df75c7c02b6a 100644 --- a/internal/terraform/transform_config.go +++ b/internal/terraform/transform_config.go @@ -181,6 +181,10 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er importTargets: imports, } + if r.List != nil { + abstract.generateConfigPath = t.generateConfigPathForImportTargets + } + var node dag.Vertex = abstract if f := t.Concrete; f != nil { node = f(abstract)