2023-05-11 01:38:37 -05:00
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
}
// 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 )
2023-05-11 13:18:25 -05:00
if nestedVal . IsNull ( ) {
// There is a difference between a null object, and an object with
// no attributes.
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = null\n" , name ) )
return diags
}
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = {\n" , name ) )
2023-05-11 01:38:37 -05:00
diags = diags . Append ( writeConfigAttributesFromExisting ( addr , buf , nestedVal , schema . NestedType . Attributes , indent + 2 ) )
buf . WriteString ( "}\n" )
return diags
case configschema . NestingList , configschema . NestingSet :
if schema . Sensitive || stateVal . IsMarked ( ) {
2023-05-11 13:18:25 -05:00
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = [] # sensitive\n" , name ) )
2023-05-11 01:38:37 -05:00
return diags
}
listVals := ctyCollectionValues ( stateVal . GetAttr ( name ) )
2023-05-11 13:18:25 -05:00
if listVals == nil {
// There is a difference between an empty list and a null list
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = null\n" , name ) )
return diags
}
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = [\n" , name ) )
2023-05-11 01:38:37 -05:00
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 :
if schema . Sensitive || stateVal . IsMarked ( ) {
2023-05-11 13:18:25 -05:00
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = {} # sensitive\n" , name ) )
2023-05-11 01:38:37 -05:00
return diags
}
2023-05-11 13:18:25 -05:00
attr := stateVal . GetAttr ( name )
if attr . IsNull ( ) {
// There is a difference between an empty map and a null map.
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = null\n" , name ) )
return diags
}
2023-05-11 01:38:37 -05:00
2023-05-11 13:18:25 -05:00
vals := attr . AsValueMap ( )
2023-05-11 01:38:37 -05:00
keys := make ( [ ] string , 0 , len ( vals ) )
for key := range vals {
keys = append ( keys , key )
}
sort . Strings ( keys )
2023-05-11 13:18:25 -05:00
buf . WriteString ( strings . Repeat ( " " , indent ) )
buf . WriteString ( fmt . Sprintf ( "%s = {\n" , name ) )
2023-05-11 01:38:37 -05:00
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 ) )
}
}