diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go index c59bcf124d..8acf6ed061 100644 --- a/internal/genconfig/generate_config.go +++ b/internal/genconfig/generate_config.go @@ -4,6 +4,7 @@ package genconfig import ( + "encoding/json" "fmt" "sort" "strings" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -158,15 +160,46 @@ func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *stri if attrS.Sensitive || val.IsMarked() { buf.WriteString("null # sensitive") } else { - tok := hclwrite.TokensForValue(val) - if _, err := tok.WriteTo(buf); err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Skipped part of config generation", - Detail: fmt.Sprintf("Could not create attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted.", name, addr), - Extra: err, - }) - continue + // If the value is a string storing a JSON value we want to represent it in a terraform native way + // and encapsulate it in `jsonencode` as it is the idiomatic representation + if val.IsKnown() && !val.IsNull() && val.Type() == cty.String && json.Valid([]byte(val.AsString())) { + buf.WriteString("jsonencode(") + + var ctyValue ctyjson.SimpleJSONValue + err := ctyValue.UnmarshalJSON([]byte(val.AsString())) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Failed to parse JSON", + Detail: fmt.Sprintf("Could not parse JSON value of attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted. This is most likely a bug in Terraform, please report it.", name, addr), + Extra: err, + }) + continue + } + + tok := hclwrite.TokensForValue(ctyValue.Value) + if _, err := tok.WriteTo(buf); err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Skipped part of config generation", + Detail: fmt.Sprintf("Could not create attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted.", name, addr), + Extra: err, + }) + continue + } + + buf.WriteString(")") + } else { + tok := hclwrite.TokensForValue(val) + if _, err := tok.WriteTo(buf); err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Skipped part of config generation", + Detail: fmt.Sprintf("Could not create attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted.", name, addr), + Extra: err, + }) + continue + } } } diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go index 13b5d68217..88829e8bf8 100644 --- a/internal/genconfig/generate_config_test.go +++ b/internal/genconfig/generate_config_test.go @@ -421,6 +421,121 @@ resource "tfcoremock_simple_resource" "empty" { list = null map = null single = null +}`, + }, + "simple_resource_with_stringified_json_object": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + 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.StringVal("D2320658"), + "value": cty.StringVal(`{ "0Hello": "World", "And": ["Solar", "System"], "ready": true }`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = jsonencode({ + "0Hello" = "World" + And = ["Solar", "System"] + ready = true + }) +}`, + }, + "simple_resource_with_stringified_json_array": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + 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.StringVal("D2320658"), + "value": cty.StringVal(`["Hello", "World"]`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = jsonencode(["Hello", "World"]) +}`, + }, + "simple_resource_with_malformed_json": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + 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.StringVal("D2320658"), + "value": cty.StringVal(`["Hello", "World"`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = "[\"Hello\", \"World\"" }`, }, }