diff --git a/.changes/v1.16/BUG FIXES-20260516-153412.yaml b/.changes/v1.16/BUG FIXES-20260516-153412.yaml new file mode 100644 index 000000000000..dc5503b170c8 --- /dev/null +++ b/.changes/v1.16/BUG FIXES-20260516-153412.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: '`terraform query -generate-config-out` no longer panics when the resource state contains a null nested-type attribute (for example an undeclared `timeouts` block).' +time: 2026-05-16T15:34:12+03:00 +custom: + Issue: "38569" diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index 328ba58de272..66b0bcaf0591 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -426,6 +426,113 @@ resource "tfcoremock_simple_resource" "empty" { list = null map = null single = null +}`, + }, + // Regression for #38569: when the list-resource flow asks + // genconfig to render a resource whose provider returned no + // state, the caller now passes the schema's empty value (a + // non-null object with null attributes and empty nested + // blocks) instead of a fully-null object. Make sure that input + // is rendered as well-formed HCL — equivalent to what + // "resource_with_nulls" produces for a hand-built null map. + "resource_with_empty_state": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + "single": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + Required: true, + }, + "list": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingList, + }, + Required: true, + }, + "map": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingMap, + }, + Required: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nested_single": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.NullVal(cty.String), + "single": cty.NullVal(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + }))), + "map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + }))), + "nested_single": cty.NullVal(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + list = null + map = null + single = null + value = null }`, }, "simple_resource_with_stringified_json_object": { diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 41dfe0f59533..ade122d8495d 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -984,11 +984,17 @@ func (n *NodePlannableResourceInstance) generateHCLListResourceDef(ctx EvalConte 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.NullVal(schema.Body.ImpliedType()) + // We still need to generate the resource block even if the state is + // not given, so that the import block can reference it. Use the + // schema's empty value (a non-null object with null attributes) + // rather than a null object: downstream genconfig assumes the + // parent value is non-null when descending into nested-type + // attributes, and a fully null value would panic. See issue #38569. + stateVal := schema.Body.EmptyValue() if val.Type().HasAttribute("state") { - stateVal = val.GetAttr("state") + if s := val.GetAttr("state"); !s.IsNull() { + stateVal = s + } } config, genDiags := n.generateResourceConfig(ctx, stateVal)