Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.16/BUG FIXES-20260516-153412.yaml
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions internal/genconfig/generate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,14 @@ func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.
return diags
}

// A null parent value carries no per-block data to emit; the downstream
// writeConfigNestedBlockFromExisting would skip each block anyway, but
// cty.Value.GetAttr panics on a null receiver, so we have to short-
// circuit before that call — see issue #38569.
if stateVal.IsNull() {
return diags
}

// Sort block names so the output will be consistent between runs.
for _, name := range slices.Sorted(maps.Keys(blocks)) {
blockS := blocks[name]
Expand All @@ -477,6 +485,20 @@ 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

// A null parent value here means the surrounding object has no value
// for this nested attribute (e.g. the entire resource state passed in
// from a list resource was cty.NullVal, or a null element was emitted
// inside a nested list/map). Treat it like a null nested value and
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A null resource value means the resource instance does not exist, so it should not be returned from a list resource. You can't generate config for a null resource value, because it is not there to be confnigured.

// emit a literal `null`, matching how this package handles null
// primitive attributes in writeConfigAttributesFromExisting — see
// issue #38569. cty.Value.GetAttr panics on a null receiver, so this
// guard must run before any of the per-nesting GetAttr calls below.
if stateVal.IsNull() && !schema.Sensitive {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = null\n", name))
return diags
}

switch schema.NestedType.Nesting {
case configschema.NestingSingle:
if schema.Sensitive {
Expand Down
181 changes: 181 additions & 0 deletions internal/genconfig/generate_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,187 @@ resource "tfcoremock_simple_resource" "empty" {
list = null
map = null
single = null
}`,
},
// Regression for #38569: a fully-null resource state (which can
// reach genconfig from the list-resource flow when the provider
// returned no state for a queried resource) used to panic inside
// writeConfigNestedTypeAttributeFromExisting because cty.GetAttr
// is not safe to call on a null receiver. The expected output
// mirrors how the package already handles a non-null parent with
// null nested values (see "resource_with_nulls").
"resource_with_null_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.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"value": cty.String,
"single": cty.Object(map[string]cty.Type{
"nested_id": cty.String,
}),
"list": cty.List(cty.Object(map[string]cty.Type{
"nested_id": cty.String,
})),
"map": cty.Map(cty.Object(map[string]cty.Type{
"nested_id": cty.String,
})),
"nested_single": cty.Object(map[string]cty.Type{
"nested_id": cty.String,
}),
})),
expected: `
resource "tfcoremock_simple_resource" "empty" {
list = null
Copy link
Copy Markdown
Member

@jbardin jbardin May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like you're testing for null attributes, but the linked issue is about a null nested block for timeouts.

map = null
single = null
value = null
}`,
},
// Sibling to "resource_with_null_state": the parent is non-null
// and a nested attribute carries a real value. This guards
// against the #38569 null-parent guard becoming too eager and
// silently dropping real nested data.
"resource_with_nested_value_sibling": {
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: 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,
},
},
},
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.StringVal("D2320658"),
"single": cty.ObjectVal(map[string]cty.Value{
"nested_id": cty.StringVal("hello"),
}),
"list": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"nested_id": cty.StringVal("world"),
}),
}),
}),
expected: `
resource "tfcoremock_simple_resource" "empty" {
list = [
{
nested_id = "world"
},
]
single = {
nested_id = "hello"
}
}`,
},
"simple_resource_with_stringified_json_object": {
Expand Down
Loading