github.com/opentofu/opentofu@v1.7.1/internal/command/jsonplan/plan.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 jsonplan 7 8 import ( 9 "encoding/json" 10 "fmt" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/zclconf/go-cty/cty" 16 ctyjson "github.com/zclconf/go-cty/cty/json" 17 18 "github.com/opentofu/opentofu/internal/addrs" 19 "github.com/opentofu/opentofu/internal/command/jsonchecks" 20 "github.com/opentofu/opentofu/internal/command/jsonconfig" 21 "github.com/opentofu/opentofu/internal/command/jsonstate" 22 "github.com/opentofu/opentofu/internal/configs" 23 "github.com/opentofu/opentofu/internal/plans" 24 "github.com/opentofu/opentofu/internal/states" 25 "github.com/opentofu/opentofu/internal/states/statefile" 26 "github.com/opentofu/opentofu/internal/tofu" 27 "github.com/opentofu/opentofu/version" 28 ) 29 30 // FormatVersion represents the version of the json format and will be 31 // incremented for any change to this format that requires changes to a 32 // consuming parser. 33 const ( 34 FormatVersion = "1.2" 35 36 ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update" 37 ResourceInstanceReplaceBecauseTainted = "replace_because_tainted" 38 ResourceInstanceReplaceByRequest = "replace_by_request" 39 ResourceInstanceReplaceByTriggers = "replace_by_triggers" 40 ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config" 41 ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition" 42 ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index" 43 ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key" 44 ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module" 45 ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target" 46 ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown" 47 ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending" 48 ResourceInstanceReadBecauseCheckNested = "read_because_check_nested" 49 ) 50 51 // Plan is the top-level representation of the json format of a plan. It includes 52 // the complete config and current state. 53 type Plan struct { 54 FormatVersion string `json:"format_version,omitempty"` 55 TerraformVersion string `json:"terraform_version,omitempty"` 56 Variables Variables `json:"variables,omitempty"` 57 PlannedValues StateValues `json:"planned_values,omitempty"` 58 // ResourceDrift and ResourceChanges are sorted in a user-friendly order 59 // that is undefined at this time, but consistent. 60 ResourceDrift []ResourceChange `json:"resource_drift,omitempty"` 61 ResourceChanges []ResourceChange `json:"resource_changes,omitempty"` 62 OutputChanges map[string]Change `json:"output_changes,omitempty"` 63 PriorState json.RawMessage `json:"prior_state,omitempty"` 64 Config json.RawMessage `json:"configuration,omitempty"` 65 RelevantAttributes []ResourceAttr `json:"relevant_attributes,omitempty"` 66 Checks json.RawMessage `json:"checks,omitempty"` 67 Timestamp string `json:"timestamp,omitempty"` 68 Errored bool `json:"errored"` 69 } 70 71 func newPlan() *Plan { 72 return &Plan{ 73 FormatVersion: FormatVersion, 74 } 75 } 76 77 // ResourceAttr contains the address and attribute of an external for the 78 // RelevantAttributes in the plan. 79 type ResourceAttr struct { 80 Resource string `json:"resource"` 81 Attr json.RawMessage `json:"attribute"` 82 } 83 84 // Change is the representation of a proposed change for an object. 85 type Change struct { 86 // Actions are the actions that will be taken on the object selected by the 87 // properties below. Valid actions values are: 88 // ["no-op"] 89 // ["create"] 90 // ["read"] 91 // ["update"] 92 // ["delete", "create"] 93 // ["create", "delete"] 94 // ["delete"] 95 // ["forget"] 96 // The two "replace" actions are represented in this way to allow callers to 97 // e.g. just scan the list for "delete" to recognize all three situations 98 // where the object will be deleted, allowing for any new deletion 99 // combinations that might be added in future. 100 Actions []string `json:"actions,omitempty"` 101 102 // Before and After are representations of the object value both before and 103 // after the action. For ["delete"] and ["forget"] actions, the "after" 104 // value is unset. For ["create"] the "before" is unset. For ["no-op"], the 105 // before and after values are identical. The "after" value will be 106 // incomplete if there are values within it that won't be known until after 107 // apply. 108 Before json.RawMessage `json:"before,omitempty"` 109 After json.RawMessage `json:"after,omitempty"` 110 111 // AfterUnknown is an object value with similar structure to After, but 112 // with all unknown leaf values replaced with true, and all known leaf 113 // values omitted. This can be combined with After to reconstruct a full 114 // value after the action, including values which will only be known after 115 // apply. 116 AfterUnknown json.RawMessage `json:"after_unknown,omitempty"` 117 118 // BeforeSensitive and AfterSensitive are object values with similar 119 // structure to Before and After, but with all sensitive leaf values 120 // replaced with true, and all non-sensitive leaf values omitted. These 121 // objects should be combined with Before and After to prevent accidental 122 // display of sensitive values in user interfaces. 123 BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"` 124 AfterSensitive json.RawMessage `json:"after_sensitive,omitempty"` 125 126 // ReplacePaths is an array of arrays representing a set of paths into the 127 // object value which resulted in the action being "replace". This will be 128 // omitted if the action is not replace, or if no paths caused the 129 // replacement (for example, if the resource was tainted). Each path 130 // consists of one or more steps, each of which will be a number or a 131 // string. 132 ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` 133 134 // Importing contains the import metadata about this operation. If importing 135 // is present (ie. not null) then the change is an import operation in 136 // addition to anything mentioned in the actions field. The actual contents 137 // of the Importing struct is subject to change, so downstream consumers 138 // should treat any values in here as strictly optional. 139 Importing *Importing `json:"importing,omitempty"` 140 141 // GeneratedConfig contains any HCL config generated for this resource 142 // during planning as a string. 143 // 144 // If this is populated, then Importing should also be populated but this 145 // might change in the future. However, nNot all Importing changes will 146 // contain generated config. 147 GeneratedConfig string `json:"generated_config,omitempty"` 148 } 149 150 // Importing is a nested object for the resource import metadata. 151 type Importing struct { 152 // The original ID of this resource used to target it as part of planned 153 // import operation. 154 ID string `json:"id,omitempty"` 155 } 156 157 type Output struct { 158 Sensitive bool `json:"sensitive"` 159 Type json.RawMessage `json:"type,omitempty"` 160 Value json.RawMessage `json:"value,omitempty"` 161 } 162 163 // Variables is the JSON representation of the variables provided to the current 164 // plan. 165 type Variables map[string]*Variable 166 167 type Variable struct { 168 Value json.RawMessage `json:"value,omitempty"` 169 } 170 171 // MarshalForRenderer returns the pre-json encoding changes of the requested 172 // plan, in a format available to the structured renderer. 173 // 174 // This function does a small part of the Marshal function, as it only returns 175 // the part of the plan required by the jsonformat.Plan renderer. 176 func MarshalForRenderer( 177 p *plans.Plan, 178 schemas *tofu.Schemas, 179 ) (map[string]Change, []ResourceChange, []ResourceChange, []ResourceAttr, error) { 180 output := newPlan() 181 182 var err error 183 if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { 184 return nil, nil, nil, nil, err 185 } 186 187 if output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas); err != nil { 188 return nil, nil, nil, nil, err 189 } 190 191 if len(p.DriftedResources) > 0 { 192 // In refresh-only mode, we render all resources marked as drifted, 193 // including those which have moved without other changes. In other plan 194 // modes, move-only changes will be included in the planned changes, so 195 // we skip them here. 196 var driftedResources []*plans.ResourceInstanceChangeSrc 197 if p.UIMode == plans.RefreshOnlyMode { 198 driftedResources = p.DriftedResources 199 } else { 200 for _, dr := range p.DriftedResources { 201 if dr.Action != plans.NoOp { 202 driftedResources = append(driftedResources, dr) 203 } 204 } 205 } 206 output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) 207 if err != nil { 208 return nil, nil, nil, nil, err 209 } 210 } 211 212 if err := output.marshalRelevantAttrs(p); err != nil { 213 return nil, nil, nil, nil, err 214 } 215 216 return output.OutputChanges, output.ResourceChanges, output.ResourceDrift, output.RelevantAttributes, nil 217 } 218 219 // MarshalForLog returns the original JSON compatible plan, ready for a logging 220 // package to marshal further. 221 func MarshalForLog( 222 config *configs.Config, 223 p *plans.Plan, 224 sf *statefile.File, 225 schemas *tofu.Schemas, 226 ) (*Plan, error) { 227 output := newPlan() 228 output.TerraformVersion = version.String() 229 output.Timestamp = p.Timestamp.Format(time.RFC3339) 230 output.Errored = p.Errored 231 232 err := output.marshalPlanVariables(p.VariableValues, config.Module.Variables) 233 if err != nil { 234 return nil, fmt.Errorf("error in marshalPlanVariables: %w", err) 235 } 236 237 // output.PlannedValues 238 err = output.marshalPlannedValues(p.Changes, schemas) 239 if err != nil { 240 return nil, fmt.Errorf("error in marshalPlannedValues: %w", err) 241 } 242 243 // output.ResourceDrift 244 if len(p.DriftedResources) > 0 { 245 // In refresh-only mode, we render all resources marked as drifted, 246 // including those which have moved without other changes. In other plan 247 // modes, move-only changes will be included in the planned changes, so 248 // we skip them here. 249 var driftedResources []*plans.ResourceInstanceChangeSrc 250 if p.UIMode == plans.RefreshOnlyMode { 251 driftedResources = p.DriftedResources 252 } else { 253 for _, dr := range p.DriftedResources { 254 if dr.Action != plans.NoOp { 255 driftedResources = append(driftedResources, dr) 256 } 257 } 258 } 259 output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) 260 if err != nil { 261 return nil, fmt.Errorf("error in marshaling resource drift: %w", err) 262 } 263 } 264 265 if err := output.marshalRelevantAttrs(p); err != nil { 266 return nil, fmt.Errorf("error marshaling relevant attributes for external changes: %w", err) 267 } 268 269 // output.ResourceChanges 270 if p.Changes != nil { 271 output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas) 272 if err != nil { 273 return nil, fmt.Errorf("error in marshaling resource changes: %w", err) 274 } 275 } 276 277 // output.OutputChanges 278 if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { 279 return nil, fmt.Errorf("error in marshaling output changes: %w", err) 280 } 281 282 // output.Checks 283 if p.Checks != nil && p.Checks.ConfigResults.Len() > 0 { 284 output.Checks = jsonchecks.MarshalCheckStates(p.Checks) 285 } 286 287 // output.PriorState 288 if sf != nil && !sf.State.Empty() { 289 output.PriorState, err = jsonstate.Marshal(sf, schemas) 290 if err != nil { 291 return nil, fmt.Errorf("error marshaling prior state: %w", err) 292 } 293 } 294 295 // output.Config 296 output.Config, err = jsonconfig.Marshal(config, schemas) 297 if err != nil { 298 return nil, fmt.Errorf("error marshaling config: %w", err) 299 } 300 301 return output, nil 302 } 303 304 // Marshal returns the json encoding of a tofu plan. 305 func Marshal( 306 config *configs.Config, 307 p *plans.Plan, 308 sf *statefile.File, 309 schemas *tofu.Schemas, 310 ) ([]byte, error) { 311 output, err := MarshalForLog(config, p, sf, schemas) 312 if err != nil { 313 return nil, err 314 } 315 316 return json.Marshal(output) 317 } 318 319 func (p *Plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error { 320 p.Variables = make(Variables, len(vars)) 321 322 for k, v := range vars { 323 val, err := v.Decode(cty.DynamicPseudoType) 324 if err != nil { 325 return err 326 } 327 valJSON, err := ctyjson.Marshal(val, val.Type()) 328 if err != nil { 329 return err 330 } 331 p.Variables[k] = &Variable{ 332 Value: valJSON, 333 } 334 } 335 336 // In Terraform v1.1 and earlier we had some confusion about which subsystem 337 // of Terraform was the one responsible for substituting in default values 338 // for unset module variables, with root module variables being handled in 339 // three different places while child module variables were only handled 340 // during the Terraform Core graph walk. 341 // 342 // For Terraform v1.2 and later we rationalized that by having the Terraform 343 // Core graph walk always be responsible for selecting defaults regardless 344 // of root vs. child module, but unfortunately our earlier accidental 345 // misbehavior bled out into the public interface by making the defaults 346 // show up in the "vars" map to this function. Those are now correctly 347 // omitted (so that the plan file only records the variables _actually_ 348 // set by the caller) but consumers of the JSON plan format may be depending 349 // on our old behavior and so we'll fake it here just in time so that 350 // outside consumers won't see a behavior change. 351 for name, decl := range decls { 352 if _, ok := p.Variables[name]; ok { 353 continue 354 } 355 if val := decl.Default; val != cty.NilVal { 356 valJSON, err := ctyjson.Marshal(val, val.Type()) 357 if err != nil { 358 return err 359 } 360 p.Variables[name] = &Variable{ 361 Value: valJSON, 362 } 363 } 364 } 365 366 if len(p.Variables) == 0 { 367 p.Variables = nil // omit this property if there are no variables to describe 368 } 369 370 return nil 371 } 372 373 // MarshalResourceChanges converts the provided internal representation of 374 // ResourceInstanceChangeSrc objects into the public structured JSON changes. 375 // 376 // This function is referenced directly from the structured renderer tests, to 377 // ensure parity between the renderers. It probably shouldn't be used anywhere 378 // else. 379 func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *tofu.Schemas) ([]ResourceChange, error) { 380 var ret []ResourceChange 381 382 var sortedResources []*plans.ResourceInstanceChangeSrc 383 sortedResources = append(sortedResources, resources...) 384 sort.Slice(sortedResources, func(i, j int) bool { 385 if !sortedResources[i].Addr.Equal(sortedResources[j].Addr) { 386 return sortedResources[i].Addr.Less(sortedResources[j].Addr) 387 } 388 return sortedResources[i].DeposedKey < sortedResources[j].DeposedKey 389 }) 390 391 for _, rc := range sortedResources { 392 var r ResourceChange 393 addr := rc.Addr 394 r.Address = addr.String() 395 if !addr.Equal(rc.PrevRunAddr) { 396 r.PreviousAddress = rc.PrevRunAddr.String() 397 } 398 399 dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode 400 // We create "delete" actions for data resources so we can clean up 401 // their entries in state, but this is an implementation detail that 402 // users shouldn't see. 403 if dataSource && rc.Action == plans.Delete { 404 continue 405 } 406 407 schema, _ := schemas.ResourceTypeConfig( 408 rc.ProviderAddr.Provider, 409 addr.Resource.Resource.Mode, 410 addr.Resource.Resource.Type, 411 ) 412 if schema == nil { 413 return nil, fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) 414 } 415 416 changeV, err := rc.Decode(schema.ImpliedType()) 417 if err != nil { 418 return nil, err 419 } 420 // We drop the marks from the change, as decoding is only an 421 // intermediate step to re-encode the values as json 422 changeV.Before, _ = changeV.Before.UnmarkDeep() 423 changeV.After, _ = changeV.After.UnmarkDeep() 424 425 var before, after []byte 426 var beforeSensitive, afterSensitive []byte 427 var afterUnknown cty.Value 428 429 if changeV.Before != cty.NilVal { 430 before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) 431 if err != nil { 432 return nil, err 433 } 434 marks := rc.BeforeValMarks 435 if schema.ContainsSensitive() { 436 marks = append(marks, schema.ValueMarks(changeV.Before, nil)...) 437 } 438 bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks)) 439 beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) 440 if err != nil { 441 return nil, err 442 } 443 } 444 if changeV.After != cty.NilVal { 445 if changeV.After.IsWhollyKnown() { 446 after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) 447 if err != nil { 448 return nil, err 449 } 450 afterUnknown = cty.EmptyObjectVal 451 } else { 452 filteredAfter := omitUnknowns(changeV.After) 453 if filteredAfter.IsNull() { 454 after = nil 455 } else { 456 after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) 457 if err != nil { 458 return nil, err 459 } 460 } 461 afterUnknown = unknownAsBool(changeV.After) 462 } 463 marks := rc.AfterValMarks 464 if schema.ContainsSensitive() { 465 marks = append(marks, schema.ValueMarks(changeV.After, nil)...) 466 } 467 as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks)) 468 afterSensitive, err = ctyjson.Marshal(as, as.Type()) 469 if err != nil { 470 return nil, err 471 } 472 } 473 474 a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) 475 if err != nil { 476 return nil, err 477 } 478 replacePaths, err := encodePaths(rc.RequiredReplace) 479 if err != nil { 480 return nil, err 481 } 482 483 var importing *Importing 484 if rc.Importing != nil { 485 importing = &Importing{ID: rc.Importing.ID} 486 } 487 488 r.Change = Change{ 489 Actions: actionString(rc.Action.String()), 490 Before: json.RawMessage(before), 491 After: json.RawMessage(after), 492 AfterUnknown: a, 493 BeforeSensitive: json.RawMessage(beforeSensitive), 494 AfterSensitive: json.RawMessage(afterSensitive), 495 ReplacePaths: replacePaths, 496 Importing: importing, 497 GeneratedConfig: rc.GeneratedConfig, 498 } 499 500 if rc.DeposedKey != states.NotDeposed { 501 r.Deposed = rc.DeposedKey.String() 502 } 503 504 key := addr.Resource.Key 505 if key != nil { 506 value := key.Value() 507 if r.Index, err = ctyjson.Marshal(value, value.Type()); err != nil { 508 return nil, err 509 } 510 } 511 512 switch addr.Resource.Resource.Mode { 513 case addrs.ManagedResourceMode: 514 r.Mode = jsonstate.ManagedResourceMode 515 case addrs.DataResourceMode: 516 r.Mode = jsonstate.DataResourceMode 517 default: 518 return nil, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String()) 519 } 520 r.ModuleAddress = addr.Module.String() 521 r.Name = addr.Resource.Resource.Name 522 r.Type = addr.Resource.Resource.Type 523 r.ProviderName = rc.ProviderAddr.Provider.String() 524 525 switch rc.ActionReason { 526 case plans.ResourceInstanceChangeNoReason: 527 r.ActionReason = "" // will be omitted in output 528 case plans.ResourceInstanceReplaceBecauseCannotUpdate: 529 r.ActionReason = ResourceInstanceReplaceBecauseCannotUpdate 530 case plans.ResourceInstanceReplaceBecauseTainted: 531 r.ActionReason = ResourceInstanceReplaceBecauseTainted 532 case plans.ResourceInstanceReplaceByRequest: 533 r.ActionReason = ResourceInstanceReplaceByRequest 534 case plans.ResourceInstanceReplaceByTriggers: 535 r.ActionReason = ResourceInstanceReplaceByTriggers 536 case plans.ResourceInstanceDeleteBecauseNoResourceConfig: 537 r.ActionReason = ResourceInstanceDeleteBecauseNoResourceConfig 538 case plans.ResourceInstanceDeleteBecauseWrongRepetition: 539 r.ActionReason = ResourceInstanceDeleteBecauseWrongRepetition 540 case plans.ResourceInstanceDeleteBecauseCountIndex: 541 r.ActionReason = ResourceInstanceDeleteBecauseCountIndex 542 case plans.ResourceInstanceDeleteBecauseEachKey: 543 r.ActionReason = ResourceInstanceDeleteBecauseEachKey 544 case plans.ResourceInstanceDeleteBecauseNoModule: 545 r.ActionReason = ResourceInstanceDeleteBecauseNoModule 546 case plans.ResourceInstanceDeleteBecauseNoMoveTarget: 547 r.ActionReason = ResourceInstanceDeleteBecauseNoMoveTarget 548 case plans.ResourceInstanceReadBecauseConfigUnknown: 549 r.ActionReason = ResourceInstanceReadBecauseConfigUnknown 550 case plans.ResourceInstanceReadBecauseDependencyPending: 551 r.ActionReason = ResourceInstanceReadBecauseDependencyPending 552 case plans.ResourceInstanceReadBecauseCheckNested: 553 r.ActionReason = ResourceInstanceReadBecauseCheckNested 554 default: 555 return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) 556 } 557 558 ret = append(ret, r) 559 560 } 561 562 return ret, nil 563 } 564 565 // MarshalOutputChanges converts the provided internal representation of 566 // Changes objects into the structured JSON representation. 567 // 568 // This function is referenced directly from the structured renderer tests, to 569 // ensure parity between the renderers. It probably shouldn't be used anywhere 570 // else. 571 func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) { 572 if changes == nil { 573 // Nothing to do! 574 return nil, nil 575 } 576 577 outputChanges := make(map[string]Change, len(changes.Outputs)) 578 for _, oc := range changes.Outputs { 579 580 // Skip output changes that are not from the root module. 581 // These are automatically stripped from plans that are written to disk 582 // elsewhere, we just need to duplicate the logic here in case anyone 583 // is converting this plan directly from memory. 584 if !oc.Addr.Module.IsRoot() { 585 continue 586 } 587 588 changeV, err := oc.Decode() 589 if err != nil { 590 return nil, err 591 } 592 // We drop the marks from the change, as decoding is only an 593 // intermediate step to re-encode the values as json 594 changeV.Before, _ = changeV.Before.UnmarkDeep() 595 changeV.After, _ = changeV.After.UnmarkDeep() 596 597 var before, after []byte 598 var afterUnknown cty.Value 599 600 if changeV.Before != cty.NilVal { 601 before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) 602 if err != nil { 603 return nil, err 604 } 605 } 606 if changeV.After != cty.NilVal { 607 if changeV.After.IsWhollyKnown() { 608 after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) 609 if err != nil { 610 return nil, err 611 } 612 afterUnknown = cty.False 613 } else { 614 filteredAfter := omitUnknowns(changeV.After) 615 if filteredAfter.IsNull() { 616 after = nil 617 } else { 618 after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) 619 if err != nil { 620 return nil, err 621 } 622 } 623 afterUnknown = unknownAsBool(changeV.After) 624 } 625 } 626 627 // The only information we have in the plan about output sensitivity is 628 // a boolean which is true if the output was or is marked sensitive. As 629 // a result, BeforeSensitive and AfterSensitive will be identical, and 630 // either false or true. 631 outputSensitive := cty.False 632 if oc.Sensitive { 633 outputSensitive = cty.True 634 } 635 sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type()) 636 if err != nil { 637 return nil, err 638 } 639 640 a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) 641 642 c := Change{ 643 Actions: actionString(oc.Action.String()), 644 Before: json.RawMessage(before), 645 After: json.RawMessage(after), 646 AfterUnknown: a, 647 BeforeSensitive: json.RawMessage(sensitive), 648 AfterSensitive: json.RawMessage(sensitive), 649 650 // Just to be explicit, outputs cannot be imported so this is always 651 // nil. 652 Importing: nil, 653 } 654 655 outputChanges[oc.Addr.OutputValue.Name] = c 656 } 657 658 return outputChanges, nil 659 } 660 661 func (p *Plan) marshalPlannedValues(changes *plans.Changes, schemas *tofu.Schemas) error { 662 // marshal the planned changes into a module 663 plan, err := marshalPlannedValues(changes, schemas) 664 if err != nil { 665 return err 666 } 667 p.PlannedValues.RootModule = plan 668 669 // marshalPlannedOutputs 670 outputs, err := marshalPlannedOutputs(changes) 671 if err != nil { 672 return err 673 } 674 p.PlannedValues.Outputs = outputs 675 676 return nil 677 } 678 679 func (p *Plan) marshalRelevantAttrs(plan *plans.Plan) error { 680 for _, ra := range plan.RelevantAttributes { 681 addr := ra.Resource.String() 682 path, err := encodePath(ra.Attr) 683 if err != nil { 684 return err 685 } 686 687 p.RelevantAttributes = append(p.RelevantAttributes, ResourceAttr{addr, path}) 688 } 689 690 // We sort the relevant attributes by resource address to make the output 691 // deterministic. Our own equivalence tests rely on it. 692 sort.Slice(p.RelevantAttributes, func(i, j int) bool { 693 return p.RelevantAttributes[i].Resource < p.RelevantAttributes[j].Resource 694 }) 695 696 return nil 697 } 698 699 // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, 700 // omitting any unknowns. 701 // 702 // The result also normalizes some types: all sequence types are turned into 703 // tuple types and all mapping types are converted to object types, since we 704 // assume the result of this is just going to be serialized as JSON (and thus 705 // lose those distinctions) anyway. 706 func omitUnknowns(val cty.Value) cty.Value { 707 ty := val.Type() 708 switch { 709 case val.IsNull(): 710 return val 711 case !val.IsKnown(): 712 return cty.NilVal 713 case ty.IsPrimitiveType(): 714 return val 715 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 716 var vals []cty.Value 717 it := val.ElementIterator() 718 for it.Next() { 719 _, v := it.Element() 720 newVal := omitUnknowns(v) 721 if newVal != cty.NilVal { 722 vals = append(vals, newVal) 723 } else if newVal == cty.NilVal { 724 // element order is how we correlate unknownness, so we must 725 // replace unknowns with nulls 726 vals = append(vals, cty.NullVal(v.Type())) 727 } 728 } 729 // We use tuple types always here, because the work we did above 730 // may have caused the individual elements to have different types, 731 // and we're doing this work to produce JSON anyway and JSON marshalling 732 // represents all of these sequence types as an array. 733 return cty.TupleVal(vals) 734 case ty.IsMapType() || ty.IsObjectType(): 735 vals := make(map[string]cty.Value) 736 it := val.ElementIterator() 737 for it.Next() { 738 k, v := it.Element() 739 newVal := omitUnknowns(v) 740 if newVal != cty.NilVal { 741 vals[k.AsString()] = newVal 742 } 743 } 744 // We use object types always here, because the work we did above 745 // may have caused the individual elements to have different types, 746 // and we're doing this work to produce JSON anyway and JSON marshalling 747 // represents both of these mapping types as an object. 748 return cty.ObjectVal(vals) 749 default: 750 // Should never happen, since the above should cover all types 751 panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) 752 } 753 } 754 755 // recursively iterate through a cty.Value, replacing unknown values (including 756 // null) with cty.True and known values with cty.False. 757 // 758 // The result also normalizes some types: all sequence types are turned into 759 // tuple types and all mapping types are converted to object types, since we 760 // assume the result of this is just going to be serialized as JSON (and thus 761 // lose those distinctions) anyway. 762 // 763 // For map/object values, all known attribute values will be omitted instead of 764 // returning false, as this results in a more compact serialization. 765 func unknownAsBool(val cty.Value) cty.Value { 766 ty := val.Type() 767 switch { 768 case val.IsNull(): 769 return cty.False 770 case !val.IsKnown(): 771 if ty.IsPrimitiveType() || ty.Equals(cty.DynamicPseudoType) { 772 return cty.True 773 } 774 fallthrough 775 case ty.IsPrimitiveType(): 776 return cty.BoolVal(!val.IsKnown()) 777 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 778 length := val.LengthInt() 779 if length == 0 { 780 // If there are no elements then we can't have unknowns 781 return cty.EmptyTupleVal 782 } 783 vals := make([]cty.Value, 0, length) 784 it := val.ElementIterator() 785 for it.Next() { 786 _, v := it.Element() 787 vals = append(vals, unknownAsBool(v)) 788 } 789 // The above transform may have changed the types of some of the 790 // elements, so we'll always use a tuple here in case we've now made 791 // different elements have different types. Our ultimate goal is to 792 // marshal to JSON anyway, and all of these sequence types are 793 // indistinguishable in JSON. 794 return cty.TupleVal(vals) 795 case ty.IsMapType() || ty.IsObjectType(): 796 var length int 797 switch { 798 case ty.IsMapType(): 799 length = val.LengthInt() 800 default: 801 length = len(val.Type().AttributeTypes()) 802 } 803 if length == 0 { 804 // If there are no elements then we can't have unknowns 805 return cty.EmptyObjectVal 806 } 807 vals := make(map[string]cty.Value) 808 it := val.ElementIterator() 809 for it.Next() { 810 k, v := it.Element() 811 vAsBool := unknownAsBool(v) 812 // Omit all of the "false"s for known values for more compact 813 // serialization 814 if !vAsBool.RawEquals(cty.False) { 815 vals[k.AsString()] = vAsBool 816 } 817 } 818 // The above transform may have changed the types of some of the 819 // elements, so we'll always use an object here in case we've now made 820 // different elements have different types. Our ultimate goal is to 821 // marshal to JSON anyway, and all of these mapping types are 822 // indistinguishable in JSON. 823 return cty.ObjectVal(vals) 824 default: 825 // Should never happen, since the above should cover all types 826 panic(fmt.Sprintf("unknownAsBool cannot handle %#v", val)) 827 } 828 } 829 830 func actionString(action string) []string { 831 switch { 832 case action == "NoOp": 833 return []string{"no-op"} 834 case action == "Create": 835 return []string{"create"} 836 case action == "Delete": 837 return []string{"delete"} 838 case action == "Update": 839 return []string{"update"} 840 case action == "CreateThenDelete": 841 return []string{"create", "delete"} 842 case action == "Read": 843 return []string{"read"} 844 case action == "DeleteThenCreate": 845 return []string{"delete", "create"} 846 case action == "Forget": 847 return []string{"forget"} 848 default: 849 return []string{action} 850 } 851 } 852 853 // UnmarshalActions reverses the actionString function. 854 func UnmarshalActions(actions []string) plans.Action { 855 if len(actions) == 2 { 856 if actions[0] == "create" && actions[1] == "delete" { 857 return plans.CreateThenDelete 858 } 859 860 if actions[0] == "delete" && actions[1] == "create" { 861 return plans.DeleteThenCreate 862 } 863 } 864 865 if len(actions) == 1 { 866 switch actions[0] { 867 case "create": 868 return plans.Create 869 case "delete": 870 return plans.Delete 871 case "update": 872 return plans.Update 873 case "read": 874 return plans.Read 875 case "no-op": 876 return plans.NoOp 877 case "forget": 878 return plans.Forget 879 } 880 } 881 882 panic("unrecognized action slice: " + strings.Join(actions, ", ")) 883 } 884 885 // encodePaths lossily encodes a cty.PathSet into an array of arrays of step 886 // values, such as: 887 // 888 // [["length"],["triggers",0,"value"]] 889 // 890 // The lossiness is that we cannot distinguish between an IndexStep with string 891 // key and a GetAttr step. This is fine with JSON output, because JSON's type 892 // system means that those two steps are equivalent anyway: both are object 893 // indexes. 894 // 895 // JavaScript (or similar dynamic language) consumers of these values can 896 // iterate over the steps starting from the root object to reach the 897 // value that each path is describing. 898 func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { 899 if pathSet.Empty() { 900 return nil, nil 901 } 902 903 pathList := pathSet.List() 904 jsonPaths := make([]json.RawMessage, 0, len(pathList)) 905 906 for _, path := range pathList { 907 jsonPath, err := encodePath(path) 908 if err != nil { 909 return nil, err 910 } 911 jsonPaths = append(jsonPaths, jsonPath) 912 } 913 914 return json.Marshal(jsonPaths) 915 } 916 917 func encodePath(path cty.Path) (json.RawMessage, error) { 918 steps := make([]json.RawMessage, 0, len(path)) 919 for _, step := range path { 920 switch s := step.(type) { 921 case cty.IndexStep: 922 key, err := ctyjson.Marshal(s.Key, s.Key.Type()) 923 if err != nil { 924 return nil, fmt.Errorf("Failed to marshal index step key %#v: %w", s.Key, err) 925 } 926 steps = append(steps, key) 927 case cty.GetAttrStep: 928 name, err := json.Marshal(s.Name) 929 if err != nil { 930 return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %w", s.Name, err) 931 } 932 steps = append(steps, name) 933 default: 934 return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) 935 } 936 } 937 return json.Marshal(steps) 938 }