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