opentofu/internal/genconfig/generate_config.go
Liam Cervante 79f7f59155
Plannable import: Generate config for imported resources during the plan. (#33153)
* command: keep our promises

* remove some nil config checks

Remove some of the safety checks that ensure plan nodes have config attached at the appropriate time.

* add GeneratedConfig to plan changes objects

Add a new GeneratedConfig field alongside Importing in plan changes.

* add config generation package

The genconfig package implements HCL config generation from provider state values.

Thanks to @mildwonkey whose implementation of terraform add is the basis for this package.

* generate config during plan

If a resource is being imported and does not already have config, attempt to generate that config during planning. The config is generated from the state as an HCL string, and then parsed back into an hcl.Body to attach to the plan graph node.

The generated config string is attached to the change emitted by the plan.

* complete config generation prototype, and add tests

---------

Co-authored-by: Katy Moe <katy@katy.moe>
2023-05-11 08:38:37 +02:00

565 lines
19 KiB
Go

package genconfig
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// GenerateResourceContents generates HCL configuration code for the provided
// resource and state value.
//
// If you want tot generate actual valid Terraform code you should follow this
// call up with a call to WrapResourceContents, which will place a Terraform
// resource header around the attributes and blocks returned by this function.
func GenerateResourceContents(addr addrs.AbsResourceInstance,
schema *configschema.Block,
pc addrs.LocalProviderConfig,
stateVal cty.Value) (string, tfdiags.Diagnostics) {
var buf strings.Builder
var diags tfdiags.Diagnostics
if pc.LocalName != addr.Resource.Resource.ImpliedProvider() || pc.Alias != "" {
buf.WriteString(strings.Repeat(" ", 2))
buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact()))
}
stateVal = omitUnknowns(stateVal)
if stateVal.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))
}
// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted), diags
}
func WrapResourceContents(addr addrs.AbsResourceInstance, config string) string {
var buf strings.Builder
buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name))
buf.WriteString(config)
buf.WriteString("}")
// The output better be valid HCL which can be parsed and formatted.
formatted := hclwrite.Format([]byte(buf.String()))
return string(formatted)
}
func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(attrs) == 0 {
return diags
}
// Get a list of sorted attribute names so the output will be consistent between runs.
keys := make([]string, 0, len(attrs))
for k := range attrs {
keys = append(keys, k)
}
sort.Strings(keys)
for i := range keys {
name := keys[i]
attrS := attrs[name]
if attrS.NestedType != nil {
diags = diags.Append(writeConfigNestedTypeAttribute(addr, buf, name, attrS, indent))
continue
}
if attrS.Required {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = ", name))
tok := hclwrite.TokensForValue(attrS.EmptyValue())
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
}
writeAttrTypeConstraint(buf, attrS)
} else if attrS.Optional {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = ", name))
tok := hclwrite.TokensForValue(attrS.EmptyValue())
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
}
writeAttrTypeConstraint(buf, attrS)
}
}
return diags
}
func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(attrs) == 0 {
return diags
}
// Get a list of sorted attribute names so the output will be consistent between runs.
keys := make([]string, 0, len(attrs))
for k := range attrs {
keys = append(keys, k)
}
sort.Strings(keys)
for i := range keys {
name := keys[i]
attrS := attrs[name]
if attrS.NestedType != nil {
writeConfigNestedTypeAttributeFromExisting(addr, buf, name, attrS, stateVal, indent)
continue
}
// Exclude computed-only attributes
if attrS.Required || attrS.Optional {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = ", name))
var val cty.Value
if stateVal.Type().HasAttribute(name) {
val = stateVal.GetAttr(name)
} else {
val = attrS.EmptyValue()
}
if val.Type() == cty.String {
// SHAMELESS HACK: If we have "" for an optional value, assume
// it is actually null, due to the legacy SDK.
if !val.IsNull() && attrS.Optional && len(val.AsString()) == 0 {
val = attrS.EmptyValue()
}
}
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
}
}
buf.WriteString("\n")
}
}
return diags
}
func writeConfigBlocks(addr addrs.AbsResourceInstance, buf *strings.Builder, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(blocks) == 0 {
return diags
}
// Get a list of sorted block names so the output will be consistent between runs.
names := make([]string, 0, len(blocks))
for k := range blocks {
names = append(names, k)
}
sort.Strings(names)
for i := range names {
name := names[i]
blockS := blocks[name]
diags = diags.Append(writeConfigNestedBlock(addr, buf, name, blockS, indent))
}
return diags
}
func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {", name))
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
buf.WriteString("}\n")
return diags
case configschema.NestingList, configschema.NestingSet:
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {", name))
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
buf.WriteString("}\n")
return diags
case configschema.NestingMap:
buf.WriteString(strings.Repeat(" ", indent))
// we use an arbitrary placeholder key (block label) "key"
buf.WriteString(fmt.Sprintf("%s \"key\" {", name))
writeBlockTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2))
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
return diags
default:
// This should not happen, the above should be exhaustive.
panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String()))
}
}
func writeConfigNestedTypeAttribute(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = ", name))
switch schema.NestedType.Nesting {
case configschema.NestingSingle:
buf.WriteString("{")
writeAttrTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2))
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
return diags
case configschema.NestingList, configschema.NestingSet:
buf.WriteString("[{")
writeAttrTypeConstraint(buf, schema)
diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2))
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}]\n")
return diags
case configschema.NestingMap:
buf.WriteString("{")
writeAttrTypeConstraint(buf, schema)
buf.WriteString(strings.Repeat(" ", indent+2))
// we use an arbitrary placeholder key "key"
buf.WriteString("key = {\n")
diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+4))
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString("}\n")
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
return diags
default:
// This should not happen, the above should be exhaustive.
panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String()))
}
}
func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(blocks) == 0 {
return diags
}
// Get a list of sorted block names so the output will be consistent between runs.
names := make([]string, 0, len(blocks))
for k := range blocks {
names = append(names, k)
}
sort.Strings(names)
for _, name := range names {
blockS := blocks[name]
// This shouldn't happen in real usage; state always has all values (set
// to null as needed), but it protects against panics in tests (and any
// really weird and unlikely cases).
if !stateVal.Type().HasAttribute(name) {
continue
}
blockVal := stateVal.GetAttr(name)
diags = diags.Append(writeConfigNestedBlockFromExisting(addr, buf, name, blockS, blockVal, indent))
}
return diags
}
func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
switch schema.NestedType.Nesting {
case configschema.NestingSingle:
if schema.Sensitive || stateVal.IsMarked() {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name))
return diags
}
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = {\n", name))
// This shouldn't happen in real usage; state always has all values (set
// to null as needed), but it protects against panics in tests (and any
// really weird and unlikely cases).
if !stateVal.Type().HasAttribute(name) {
return diags
}
nestedVal := stateVal.GetAttr(name)
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2))
buf.WriteString("}\n")
return diags
case configschema.NestingList, configschema.NestingSet:
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = [", name))
if schema.Sensitive || stateVal.IsMarked() {
buf.WriteString("] # sensitive\n")
return diags
}
buf.WriteString("\n")
listVals := ctyCollectionValues(stateVal.GetAttr(name))
for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent+2))
// The entire element is marked.
if listVals[i].IsMarked() {
buf.WriteString("{}, # sensitive\n")
continue
}
buf.WriteString("{\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4))
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString("},\n")
}
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("]\n")
return diags
case configschema.NestingMap:
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s = {", name))
if schema.Sensitive || stateVal.IsMarked() {
buf.WriteString(" } # sensitive\n")
return diags
}
buf.WriteString("\n")
vals := stateVal.GetAttr(name).AsValueMap()
keys := make([]string, 0, len(vals))
for key := range vals {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(fmt.Sprintf("%s = {", key))
// This entire value is marked
if vals[key].IsMarked() {
buf.WriteString("} # sensitive\n")
continue
}
buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4))
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString("}\n")
}
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
return diags
default:
// This should not happen, the above should be exhaustive.
panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String()))
}
}
func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {", name))
// If the entire value is marked, don't print any nested attributes
if stateVal.IsMarked() {
buf.WriteString("} # sensitive\n")
return diags
}
buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, stateVal, schema.BlockTypes, indent+2))
buf.WriteString("}\n")
return diags
case configschema.NestingList, configschema.NestingSet:
if stateVal.IsMarked() {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name))
return diags
}
listVals := ctyCollectionValues(stateVal)
for i := range listVals {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s {\n", name))
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2))
buf.WriteString("}\n")
}
return diags
case configschema.NestingMap:
// If the entire value is marked, don't print any nested attributes
if stateVal.IsMarked() {
buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name))
return diags
}
vals := stateVal.AsValueMap()
keys := make([]string, 0, len(vals))
for key := range vals {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString(fmt.Sprintf("%s %q {", name, key))
// This entire map element is marked
if vals[key].IsMarked() {
buf.WriteString("} # sensitive\n")
return diags
}
buf.WriteString("\n")
diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2))
diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, vals[key], schema.BlockTypes, indent+2))
buf.WriteString(strings.Repeat(" ", indent))
buf.WriteString("}\n")
}
return diags
default:
// This should not happen, the above should be exhaustive.
panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String()))
}
}
func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribute) {
if schema.Required {
buf.WriteString(" # REQUIRED ")
} else {
buf.WriteString(" # OPTIONAL ")
}
if schema.NestedType != nil {
buf.WriteString(fmt.Sprintf("%s\n", schema.NestedType.ImpliedType().FriendlyName()))
} else {
buf.WriteString(fmt.Sprintf("%s\n", schema.Type.FriendlyName()))
}
}
func writeBlockTypeConstraint(buf *strings.Builder, schema *configschema.NestedBlock) {
if schema.MinItems > 0 {
buf.WriteString(" # REQUIRED block\n")
} else {
buf.WriteString(" # OPTIONAL block\n")
}
}
// copied from command/format/diff
func ctyCollectionValues(val cty.Value) []cty.Value {
if !val.IsKnown() || val.IsNull() {
return nil
}
var len int
if val.IsMarked() {
val, _ = val.Unmark()
len = val.LengthInt()
} else {
len = val.LengthInt()
}
ret := make([]cty.Value, 0, len)
for it := val.ElementIterator(); it.Next(); {
_, value := it.Element()
ret = append(ret, value)
}
return ret
}
// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value,
// omitting any unknowns.
//
// The result also normalizes some types: all sequence types are turned into
// tuple types and all mapping types are converted to object types, since we
// assume the result of this is just going to be serialized as JSON (and thus
// lose those distinctions) anyway.
func omitUnknowns(val cty.Value) cty.Value {
ty := val.Type()
switch {
case val.IsNull():
return val
case !val.IsKnown():
return cty.NilVal
case ty.IsPrimitiveType():
return val
case ty.IsListType() || ty.IsTupleType() || ty.IsSetType():
var vals []cty.Value
it := val.ElementIterator()
for it.Next() {
_, v := it.Element()
newVal := omitUnknowns(v)
if newVal != cty.NilVal {
vals = append(vals, newVal)
} else if newVal == cty.NilVal {
// element order is how we correlate unknownness, so we must
// replace unknowns with nulls
vals = append(vals, cty.NullVal(v.Type()))
}
}
// We use tuple types always here, because the work we did above
// may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling
// represents all of these sequence types as an array.
return cty.TupleVal(vals)
case ty.IsMapType() || ty.IsObjectType():
vals := make(map[string]cty.Value)
it := val.ElementIterator()
for it.Next() {
k, v := it.Element()
newVal := omitUnknowns(v)
if newVal != cty.NilVal {
vals[k.AsString()] = newVal
}
}
// We use object types always here, because the work we did above
// may have caused the individual elements to have different types,
// and we're doing this work to produce JSON anyway and JSON marshalling
// represents both of these mapping types as an object.
return cty.ObjectVal(vals)
default:
// Should never happen, since the above should cover all types
panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val))
}
}