diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go index 97bdd2a5cf..80a6f01734 100644 --- a/internal/genconfig/generate_config.go +++ b/internal/genconfig/generate_config.go @@ -103,7 +103,7 @@ func (i ImportGroup) ImportsString() string { func GenerateResourceContents(addr addrs.AbsResourceInstance, schema *configschema.Block, pc addrs.LocalProviderConfig, - stateVal cty.Value, + configVal cty.Value, forceProviderAddr bool, ) (Resource, tfdiags.Diagnostics) { var buf strings.Builder @@ -117,19 +117,12 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance, buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact())) } - // This is generating configuration, so the only marks should be coming from - // the schema itself. - stateVal, _ = stateVal.UnmarkDeep() - - // filter the state down to a suitable config value - stateVal = extractConfigFromState(schema, stateVal) - - if stateVal.RawEquals(cty.NilVal) { + if configVal.RawEquals(cty.NilVal) { 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(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, configVal, schema.Attributes, 2)) + diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, configVal, schema.BlockTypes, 2)) } // The output better be valid HCL which can be parsed and formatted. @@ -137,27 +130,27 @@ func GenerateResourceContents(addr addrs.AbsResourceInstance, return Resource{Addr: addr, Body: formatted}, diags } +// ResourceListElement is a single Resource state and identity pair derived from +// a list resource response. +type ResourceListElement struct { + // Config is the cty value extracted from the resource state which is + // intended to be written into the HCL resource block. + Config cty.Value + + Identity cty.Value +} + func GenerateListResourceContents(addr addrs.AbsResourceInstance, schema *configschema.Block, idSchema *configschema.Object, pc addrs.LocalProviderConfig, - stateVal cty.Value, + resources []ResourceListElement, ) (ImportGroup, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics ret := ImportGroup{} - 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 ret, diags - } - - iter := stateVal.ElementIterator() - for idx := 0; iter.Next(); idx++ { + for idx, res := range resources { // Generate a unique resource name for each instance in the list. resAddr := addrs.AbsResourceInstance{ Module: addr.Module, @@ -171,14 +164,7 @@ func GenerateListResourceContents(addr addrs.AbsResourceInstance, }, } - _, 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, true) + content, gDiags := GenerateResourceContents(resAddr, schema, pc, res.Config, true) if gDiags.HasErrors() { diags = diags.Append(gDiags) continue @@ -191,8 +177,7 @@ func GenerateListResourceContents(addr addrs.AbsResourceInstance, }, } - idVal := val.GetAttr("identity") - importContent, gDiags := GenerateImportBlock(resAddr, idSchema, pc, idVal) + importContent, gDiags := GenerateImportBlock(resAddr, idSchema, pc, res.Identity) if gDiags.HasErrors() { diags = diags.Append(gDiags) continue @@ -667,11 +652,11 @@ func hclEscapeString(str string) string { return str } -// extractConfigFromState takes the state value of a resource, and filters the +// ExtractLegacyConfigFromState takes the state value of a resource, and filters the // value down to what would be acceptable as a resource configuration value. // This is used when the provider does not implement GenerateResourceConfig to // create a suitable value. -func extractConfigFromState(schema *configschema.Block, state cty.Value) cty.Value { +func ExtractLegacyConfigFromState(schema *configschema.Block, state cty.Value) cty.Value { config, _ := cty.Transform(state, func(path cty.Path, v cty.Value) (cty.Value, error) { if v.IsNull() { return v, nil diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index af2b3366ca..d3ab56635d 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -829,7 +829,11 @@ 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, false) + + val, _ := tc.value.UnmarkDeep() + config := ExtractLegacyConfigFromState(tc.schema, val) + + contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, config, false) if len(diags) > 0 { t.Errorf("expected no diagnostics but found %s", diags) } @@ -957,8 +961,29 @@ func TestGenerateResourceAndIDContents(t *testing.T) { LocalName: "aws", } + // the handling of the list value was moved to the caller, so break it back down in the same way here + var listElements []ResourceListElement + + iter := value.ElementIterator() + for iter.Next() { + _, 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") + } + + stateVal, _ = stateVal.UnmarkDeep() + config := ExtractLegacyConfigFromState(schema, stateVal) + + idVal := val.GetAttr("identity") + + listElements = append(listElements, ResourceListElement{Config: config, Identity: idVal}) + } + // Generate content - content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, value) + content, diags := GenerateListResourceContents(instAddr1, schema, idSchema, pc, listElements) // Check for diagnostics if diags.HasErrors() { t.Fatalf("unexpected diagnostics: %s", diags.Err()) diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 7047a7a091..8af9bc196b 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -890,7 +890,13 @@ func (n *NodePlannableResourceInstance) generateHCLResourceDef(addr addrs.AbsRes Alias: n.ResolvedProvider.Alias, } - return genconfig.GenerateResourceContents(addr, schema.Body, providerAddr, state, false) + // This is generating configuration, so the only marks should be coming from + // the schema itself. + state, _ = state.UnmarkDeep() + // filter the state down to a suitable config value + config := genconfig.ExtractLegacyConfigFromState(schema.Body, state) + + return genconfig.GenerateResourceContents(addr, schema.Body, providerAddr, config, false) } func (n *NodePlannableResourceInstance) generateHCLListResourceDef(addr addrs.AbsResourceInstance, state cty.Value, schema providers.Schema) (genconfig.ImportGroup, tfdiags.Diagnostics) { @@ -899,7 +905,30 @@ func (n *NodePlannableResourceInstance) generateHCLListResourceDef(addr addrs.Ab Alias: n.ResolvedProvider.Alias, } - return genconfig.GenerateListResourceContents(addr, schema.Body, schema.Identity, providerAddr, state) + if !state.CanIterateElements() { + panic(fmt.Sprintf("invalid list resource data: %#v\n", state)) + } + + var listElements []genconfig.ResourceListElement + + iter := state.ElementIterator() + for iter.Next() { + _, 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") + } + + stateVal, _ = stateVal.UnmarkDeep() + config := genconfig.ExtractLegacyConfigFromState(schema.Body, stateVal) + idVal := val.GetAttr("identity") + + listElements = append(listElements, genconfig.ResourceListElement{Config: config, Identity: idVal}) + } + + return genconfig.GenerateListResourceContents(addr, schema.Body, schema.Identity, providerAddr, listElements) } // mergeDeps returns the union of 2 sets of dependencies