Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Comment thread
jbardin marked this conversation as resolved.
Outdated
// 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
Comment thread
jbardin marked this conversation as resolved.
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