github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/genconfig/generate_config.go (about) 1 package genconfig 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/hashicorp/hcl/v2/hclwrite" 10 "github.com/zclconf/go-cty/cty" 11 12 "github.com/terramate-io/tf/addrs" 13 "github.com/terramate-io/tf/configs/configschema" 14 "github.com/terramate-io/tf/tfdiags" 15 ) 16 17 // GenerateResourceContents generates HCL configuration code for the provided 18 // resource and state value. 19 // 20 // If you want to generate actual valid Terraform code you should follow this 21 // call up with a call to WrapResourceContents, which will place a Terraform 22 // resource header around the attributes and blocks returned by this function. 23 func GenerateResourceContents(addr addrs.AbsResourceInstance, 24 schema *configschema.Block, 25 pc addrs.LocalProviderConfig, 26 stateVal cty.Value) (string, tfdiags.Diagnostics) { 27 var buf strings.Builder 28 29 var diags tfdiags.Diagnostics 30 31 if pc.LocalName != addr.Resource.Resource.ImpliedProvider() || pc.Alias != "" { 32 buf.WriteString(strings.Repeat(" ", 2)) 33 buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact())) 34 } 35 36 stateVal = omitUnknowns(stateVal) 37 if stateVal.RawEquals(cty.NilVal) { 38 diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2)) 39 diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2)) 40 } else { 41 diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2)) 42 diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2)) 43 } 44 45 // The output better be valid HCL which can be parsed and formatted. 46 formatted := hclwrite.Format([]byte(buf.String())) 47 return string(formatted), diags 48 } 49 50 func WrapResourceContents(addr addrs.AbsResourceInstance, config string) string { 51 var buf strings.Builder 52 53 buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name)) 54 buf.WriteString(config) 55 buf.WriteString("}") 56 57 // The output better be valid HCL which can be parsed and formatted. 58 formatted := hclwrite.Format([]byte(buf.String())) 59 return string(formatted) 60 } 61 62 func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { 63 var diags tfdiags.Diagnostics 64 65 if len(attrs) == 0 { 66 return diags 67 } 68 69 // Get a list of sorted attribute names so the output will be consistent between runs. 70 keys := make([]string, 0, len(attrs)) 71 for k := range attrs { 72 keys = append(keys, k) 73 } 74 sort.Strings(keys) 75 76 for i := range keys { 77 name := keys[i] 78 attrS := attrs[name] 79 if attrS.NestedType != nil { 80 diags = diags.Append(writeConfigNestedTypeAttribute(addr, buf, name, attrS, indent)) 81 continue 82 } 83 if attrS.Required { 84 buf.WriteString(strings.Repeat(" ", indent)) 85 buf.WriteString(fmt.Sprintf("%s = ", name)) 86 tok := hclwrite.TokensForValue(attrS.EmptyValue()) 87 if _, err := tok.WriteTo(buf); err != nil { 88 diags = diags.Append(&hcl.Diagnostic{ 89 Severity: hcl.DiagWarning, 90 Summary: "Skipped part of config generation", 91 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), 92 Extra: err, 93 }) 94 continue 95 } 96 writeAttrTypeConstraint(buf, attrS) 97 } else if attrS.Optional { 98 buf.WriteString(strings.Repeat(" ", indent)) 99 buf.WriteString(fmt.Sprintf("%s = ", name)) 100 tok := hclwrite.TokensForValue(attrS.EmptyValue()) 101 if _, err := tok.WriteTo(buf); err != nil { 102 diags = diags.Append(&hcl.Diagnostic{ 103 Severity: hcl.DiagWarning, 104 Summary: "Skipped part of config generation", 105 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), 106 Extra: err, 107 }) 108 continue 109 } 110 writeAttrTypeConstraint(buf, attrS) 111 } 112 } 113 return diags 114 } 115 116 func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { 117 var diags tfdiags.Diagnostics 118 if len(attrs) == 0 { 119 return diags 120 } 121 122 // Get a list of sorted attribute names so the output will be consistent between runs. 123 keys := make([]string, 0, len(attrs)) 124 for k := range attrs { 125 keys = append(keys, k) 126 } 127 sort.Strings(keys) 128 129 for i := range keys { 130 name := keys[i] 131 attrS := attrs[name] 132 if attrS.NestedType != nil { 133 writeConfigNestedTypeAttributeFromExisting(addr, buf, name, attrS, stateVal, indent) 134 continue 135 } 136 137 // Exclude computed-only attributes 138 if attrS.Required || attrS.Optional { 139 buf.WriteString(strings.Repeat(" ", indent)) 140 buf.WriteString(fmt.Sprintf("%s = ", name)) 141 142 var val cty.Value 143 if !stateVal.IsNull() && stateVal.Type().HasAttribute(name) { 144 val = stateVal.GetAttr(name) 145 } else { 146 val = attrS.EmptyValue() 147 } 148 if val.Type() == cty.String { 149 // SHAMELESS HACK: If we have "" for an optional value, assume 150 // it is actually null, due to the legacy SDK. 151 if !val.IsNull() && attrS.Optional && len(val.AsString()) == 0 { 152 val = attrS.EmptyValue() 153 } 154 } 155 if attrS.Sensitive || val.IsMarked() { 156 buf.WriteString("null # sensitive") 157 } else { 158 tok := hclwrite.TokensForValue(val) 159 if _, err := tok.WriteTo(buf); err != nil { 160 diags = diags.Append(&hcl.Diagnostic{ 161 Severity: hcl.DiagWarning, 162 Summary: "Skipped part of config generation", 163 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), 164 Extra: err, 165 }) 166 continue 167 } 168 } 169 170 buf.WriteString("\n") 171 } 172 } 173 return diags 174 } 175 176 func writeConfigBlocks(addr addrs.AbsResourceInstance, buf *strings.Builder, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics { 177 var diags tfdiags.Diagnostics 178 179 if len(blocks) == 0 { 180 return diags 181 } 182 183 // Get a list of sorted block names so the output will be consistent between runs. 184 names := make([]string, 0, len(blocks)) 185 for k := range blocks { 186 names = append(names, k) 187 } 188 sort.Strings(names) 189 190 for i := range names { 191 name := names[i] 192 blockS := blocks[name] 193 diags = diags.Append(writeConfigNestedBlock(addr, buf, name, blockS, indent)) 194 } 195 return diags 196 } 197 198 func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, indent int) tfdiags.Diagnostics { 199 var diags tfdiags.Diagnostics 200 201 switch schema.Nesting { 202 case configschema.NestingSingle, configschema.NestingGroup: 203 buf.WriteString(strings.Repeat(" ", indent)) 204 buf.WriteString(fmt.Sprintf("%s {", name)) 205 writeBlockTypeConstraint(buf, schema) 206 diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) 207 diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) 208 buf.WriteString("}\n") 209 return diags 210 case configschema.NestingList, configschema.NestingSet: 211 buf.WriteString(strings.Repeat(" ", indent)) 212 buf.WriteString(fmt.Sprintf("%s {", name)) 213 writeBlockTypeConstraint(buf, schema) 214 diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) 215 diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) 216 buf.WriteString("}\n") 217 return diags 218 case configschema.NestingMap: 219 buf.WriteString(strings.Repeat(" ", indent)) 220 // we use an arbitrary placeholder key (block label) "key" 221 buf.WriteString(fmt.Sprintf("%s \"key\" {", name)) 222 writeBlockTypeConstraint(buf, schema) 223 diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) 224 diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) 225 buf.WriteString(strings.Repeat(" ", indent)) 226 buf.WriteString("}\n") 227 return diags 228 default: 229 // This should not happen, the above should be exhaustive. 230 panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())) 231 } 232 } 233 234 func writeConfigNestedTypeAttribute(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, indent int) tfdiags.Diagnostics { 235 var diags tfdiags.Diagnostics 236 237 buf.WriteString(strings.Repeat(" ", indent)) 238 buf.WriteString(fmt.Sprintf("%s = ", name)) 239 240 switch schema.NestedType.Nesting { 241 case configschema.NestingSingle: 242 buf.WriteString("{") 243 writeAttrTypeConstraint(buf, schema) 244 diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2)) 245 buf.WriteString(strings.Repeat(" ", indent)) 246 buf.WriteString("}\n") 247 return diags 248 case configschema.NestingList, configschema.NestingSet: 249 buf.WriteString("[{") 250 writeAttrTypeConstraint(buf, schema) 251 diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2)) 252 buf.WriteString(strings.Repeat(" ", indent)) 253 buf.WriteString("}]\n") 254 return diags 255 case configschema.NestingMap: 256 buf.WriteString("{") 257 writeAttrTypeConstraint(buf, schema) 258 buf.WriteString(strings.Repeat(" ", indent+2)) 259 // we use an arbitrary placeholder key "key" 260 buf.WriteString("key = {\n") 261 diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+4)) 262 buf.WriteString(strings.Repeat(" ", indent+2)) 263 buf.WriteString("}\n") 264 buf.WriteString(strings.Repeat(" ", indent)) 265 buf.WriteString("}\n") 266 return diags 267 default: 268 // This should not happen, the above should be exhaustive. 269 panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())) 270 } 271 } 272 273 func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics { 274 var diags tfdiags.Diagnostics 275 276 if len(blocks) == 0 { 277 return diags 278 } 279 280 // Get a list of sorted block names so the output will be consistent between runs. 281 names := make([]string, 0, len(blocks)) 282 for k := range blocks { 283 names = append(names, k) 284 } 285 sort.Strings(names) 286 287 for _, name := range names { 288 blockS := blocks[name] 289 // This shouldn't happen in real usage; state always has all values (set 290 // to null as needed), but it protects against panics in tests (and any 291 // really weird and unlikely cases). 292 if !stateVal.Type().HasAttribute(name) { 293 continue 294 } 295 blockVal := stateVal.GetAttr(name) 296 diags = diags.Append(writeConfigNestedBlockFromExisting(addr, buf, name, blockS, blockVal, indent)) 297 } 298 299 return diags 300 } 301 302 func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics { 303 var diags tfdiags.Diagnostics 304 305 switch schema.NestedType.Nesting { 306 case configschema.NestingSingle: 307 if schema.Sensitive || stateVal.IsMarked() { 308 buf.WriteString(strings.Repeat(" ", indent)) 309 buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name)) 310 return diags 311 } 312 313 // This shouldn't happen in real usage; state always has all values (set 314 // to null as needed), but it protects against panics in tests (and any 315 // really weird and unlikely cases). 316 if !stateVal.Type().HasAttribute(name) { 317 return diags 318 } 319 nestedVal := stateVal.GetAttr(name) 320 321 if nestedVal.IsNull() { 322 // There is a difference between a null object, and an object with 323 // no attributes. 324 buf.WriteString(strings.Repeat(" ", indent)) 325 buf.WriteString(fmt.Sprintf("%s = null\n", name)) 326 return diags 327 } 328 329 buf.WriteString(strings.Repeat(" ", indent)) 330 buf.WriteString(fmt.Sprintf("%s = {\n", name)) 331 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2)) 332 buf.WriteString("}\n") 333 return diags 334 335 case configschema.NestingList, configschema.NestingSet: 336 337 if schema.Sensitive || stateVal.IsMarked() { 338 buf.WriteString(strings.Repeat(" ", indent)) 339 buf.WriteString(fmt.Sprintf("%s = [] # sensitive\n", name)) 340 return diags 341 } 342 343 listVals := ctyCollectionValues(stateVal.GetAttr(name)) 344 if listVals == nil { 345 // There is a difference between an empty list and a null list 346 buf.WriteString(strings.Repeat(" ", indent)) 347 buf.WriteString(fmt.Sprintf("%s = null\n", name)) 348 return diags 349 } 350 351 buf.WriteString(strings.Repeat(" ", indent)) 352 buf.WriteString(fmt.Sprintf("%s = [\n", name)) 353 for i := range listVals { 354 buf.WriteString(strings.Repeat(" ", indent+2)) 355 356 // The entire element is marked. 357 if listVals[i].IsMarked() { 358 buf.WriteString("{}, # sensitive\n") 359 continue 360 } 361 362 buf.WriteString("{\n") 363 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4)) 364 buf.WriteString(strings.Repeat(" ", indent+2)) 365 buf.WriteString("},\n") 366 } 367 buf.WriteString(strings.Repeat(" ", indent)) 368 buf.WriteString("]\n") 369 return diags 370 371 case configschema.NestingMap: 372 if schema.Sensitive || stateVal.IsMarked() { 373 buf.WriteString(strings.Repeat(" ", indent)) 374 buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name)) 375 return diags 376 } 377 378 attr := stateVal.GetAttr(name) 379 if attr.IsNull() { 380 // There is a difference between an empty map and a null map. 381 buf.WriteString(strings.Repeat(" ", indent)) 382 buf.WriteString(fmt.Sprintf("%s = null\n", name)) 383 return diags 384 } 385 386 vals := attr.AsValueMap() 387 keys := make([]string, 0, len(vals)) 388 for key := range vals { 389 keys = append(keys, key) 390 } 391 sort.Strings(keys) 392 393 buf.WriteString(strings.Repeat(" ", indent)) 394 buf.WriteString(fmt.Sprintf("%s = {\n", name)) 395 for _, key := range keys { 396 buf.WriteString(strings.Repeat(" ", indent+2)) 397 buf.WriteString(fmt.Sprintf("%s = {", key)) 398 399 // This entire value is marked 400 if vals[key].IsMarked() { 401 buf.WriteString("} # sensitive\n") 402 continue 403 } 404 405 buf.WriteString("\n") 406 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4)) 407 buf.WriteString(strings.Repeat(" ", indent+2)) 408 buf.WriteString("}\n") 409 } 410 buf.WriteString(strings.Repeat(" ", indent)) 411 buf.WriteString("}\n") 412 return diags 413 414 default: 415 // This should not happen, the above should be exhaustive. 416 panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())) 417 } 418 } 419 420 func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics { 421 var diags tfdiags.Diagnostics 422 423 switch schema.Nesting { 424 case configschema.NestingSingle, configschema.NestingGroup: 425 if stateVal.IsNull() { 426 return diags 427 } 428 buf.WriteString(strings.Repeat(" ", indent)) 429 buf.WriteString(fmt.Sprintf("%s {", name)) 430 431 // If the entire value is marked, don't print any nested attributes 432 if stateVal.IsMarked() { 433 buf.WriteString("} # sensitive\n") 434 return diags 435 } 436 buf.WriteString("\n") 437 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2)) 438 diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, stateVal, schema.BlockTypes, indent+2)) 439 buf.WriteString("}\n") 440 return diags 441 case configschema.NestingList, configschema.NestingSet: 442 if stateVal.IsMarked() { 443 buf.WriteString(strings.Repeat(" ", indent)) 444 buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name)) 445 return diags 446 } 447 listVals := ctyCollectionValues(stateVal) 448 for i := range listVals { 449 buf.WriteString(strings.Repeat(" ", indent)) 450 buf.WriteString(fmt.Sprintf("%s {\n", name)) 451 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2)) 452 diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2)) 453 buf.WriteString("}\n") 454 } 455 return diags 456 case configschema.NestingMap: 457 // If the entire value is marked, don't print any nested attributes 458 if stateVal.IsMarked() { 459 buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name)) 460 return diags 461 } 462 463 vals := stateVal.AsValueMap() 464 keys := make([]string, 0, len(vals)) 465 for key := range vals { 466 keys = append(keys, key) 467 } 468 sort.Strings(keys) 469 for _, key := range keys { 470 buf.WriteString(strings.Repeat(" ", indent)) 471 buf.WriteString(fmt.Sprintf("%s %q {", name, key)) 472 // This entire map element is marked 473 if vals[key].IsMarked() { 474 buf.WriteString("} # sensitive\n") 475 return diags 476 } 477 buf.WriteString("\n") 478 diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2)) 479 diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, vals[key], schema.BlockTypes, indent+2)) 480 buf.WriteString(strings.Repeat(" ", indent)) 481 buf.WriteString("}\n") 482 } 483 return diags 484 default: 485 // This should not happen, the above should be exhaustive. 486 panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())) 487 } 488 } 489 490 func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribute) { 491 if schema.Required { 492 buf.WriteString(" # REQUIRED ") 493 } else { 494 buf.WriteString(" # OPTIONAL ") 495 } 496 497 if schema.NestedType != nil { 498 buf.WriteString(fmt.Sprintf("%s\n", schema.NestedType.ImpliedType().FriendlyName())) 499 } else { 500 buf.WriteString(fmt.Sprintf("%s\n", schema.Type.FriendlyName())) 501 } 502 } 503 504 func writeBlockTypeConstraint(buf *strings.Builder, schema *configschema.NestedBlock) { 505 if schema.MinItems > 0 { 506 buf.WriteString(" # REQUIRED block\n") 507 } else { 508 buf.WriteString(" # OPTIONAL block\n") 509 } 510 } 511 512 // copied from command/format/diff 513 func ctyCollectionValues(val cty.Value) []cty.Value { 514 if !val.IsKnown() || val.IsNull() { 515 return nil 516 } 517 518 var len int 519 if val.IsMarked() { 520 val, _ = val.Unmark() 521 len = val.LengthInt() 522 } else { 523 len = val.LengthInt() 524 } 525 526 ret := make([]cty.Value, 0, len) 527 for it := val.ElementIterator(); it.Next(); { 528 _, value := it.Element() 529 ret = append(ret, value) 530 } 531 532 return ret 533 } 534 535 // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, 536 // omitting any unknowns. 537 // 538 // The result also normalizes some types: all sequence types are turned into 539 // tuple types and all mapping types are converted to object types, since we 540 // assume the result of this is just going to be serialized as JSON (and thus 541 // lose those distinctions) anyway. 542 func omitUnknowns(val cty.Value) cty.Value { 543 ty := val.Type() 544 switch { 545 case val.IsNull(): 546 return val 547 case !val.IsKnown(): 548 return cty.NilVal 549 case ty.IsPrimitiveType(): 550 return val 551 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 552 var vals []cty.Value 553 it := val.ElementIterator() 554 for it.Next() { 555 _, v := it.Element() 556 newVal := omitUnknowns(v) 557 if newVal != cty.NilVal { 558 vals = append(vals, newVal) 559 } else if newVal == cty.NilVal { 560 // element order is how we correlate unknownness, so we must 561 // replace unknowns with nulls 562 vals = append(vals, cty.NullVal(v.Type())) 563 } 564 } 565 // We use tuple types always here, because the work we did above 566 // may have caused the individual elements to have different types, 567 // and we're doing this work to produce JSON anyway and JSON marshalling 568 // represents all of these sequence types as an array. 569 return cty.TupleVal(vals) 570 case ty.IsMapType() || ty.IsObjectType(): 571 vals := make(map[string]cty.Value) 572 it := val.ElementIterator() 573 for it.Next() { 574 k, v := it.Element() 575 newVal := omitUnknowns(v) 576 if newVal != cty.NilVal { 577 vals[k.AsString()] = newVal 578 } 579 } 580 // We use object types always here, because the work we did above 581 // may have caused the individual elements to have different types, 582 // and we're doing this work to produce JSON anyway and JSON marshalling 583 // represents both of these mapping types as an object. 584 return cty.ObjectVal(vals) 585 default: 586 // Should never happen, since the above should cover all types 587 panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) 588 } 589 }