github.com/opentofu/opentofu@v1.7.1/internal/plans/planfile/tfplan.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 planfile 7 8 import ( 9 "fmt" 10 "io" 11 "time" 12 13 "github.com/zclconf/go-cty/cty" 14 "google.golang.org/protobuf/proto" 15 16 "github.com/opentofu/opentofu/internal/addrs" 17 "github.com/opentofu/opentofu/internal/checks" 18 "github.com/opentofu/opentofu/internal/lang/globalref" 19 "github.com/opentofu/opentofu/internal/lang/marks" 20 "github.com/opentofu/opentofu/internal/plans" 21 "github.com/opentofu/opentofu/internal/plans/internal/planproto" 22 "github.com/opentofu/opentofu/internal/states" 23 "github.com/opentofu/opentofu/version" 24 ) 25 26 const tfplanFormatVersion = 3 27 const tfplanFilename = "tfplan" 28 29 // --------------------------------------------------------------------------- 30 // This file deals with the internal structure of the "tfplan" sub-file within 31 // the plan file format. It's all private API, wrapped by methods defined 32 // elsewhere. This is the only file that should import the 33 // ../internal/planproto package, which contains the ugly stubs generated 34 // by the protobuf compiler. 35 // --------------------------------------------------------------------------- 36 37 // readTfplan reads a protobuf-encoded description from the plan portion of 38 // a plan file, which is stored in a special file in the archive called 39 // "tfplan". 40 func readTfplan(r io.Reader) (*plans.Plan, error) { 41 src, err := io.ReadAll(r) 42 if err != nil { 43 return nil, err 44 } 45 46 var rawPlan planproto.Plan 47 err = proto.Unmarshal(src, &rawPlan) 48 if err != nil { 49 return nil, fmt.Errorf("parse error: %w", err) 50 } 51 52 if rawPlan.Version != tfplanFormatVersion { 53 return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion) 54 } 55 56 if rawPlan.TerraformVersion != version.String() { 57 return nil, fmt.Errorf("plan file was created by OpenTofu or Terraform %s, but this is %s; plan files cannot be transferred between different versions of OpenTofu / Terraform", rawPlan.TerraformVersion, version.String()) 58 } 59 60 plan := &plans.Plan{ 61 VariableValues: map[string]plans.DynamicValue{}, 62 Changes: &plans.Changes{ 63 Outputs: []*plans.OutputChangeSrc{}, 64 Resources: []*plans.ResourceInstanceChangeSrc{}, 65 }, 66 DriftedResources: []*plans.ResourceInstanceChangeSrc{}, 67 Checks: &states.CheckResults{}, 68 } 69 70 plan.Errored = rawPlan.Errored 71 72 switch rawPlan.UiMode { 73 case planproto.Mode_NORMAL: 74 plan.UIMode = plans.NormalMode 75 case planproto.Mode_DESTROY: 76 plan.UIMode = plans.DestroyMode 77 case planproto.Mode_REFRESH_ONLY: 78 plan.UIMode = plans.RefreshOnlyMode 79 default: 80 return nil, fmt.Errorf("plan has invalid mode %s", rawPlan.UiMode) 81 } 82 83 for _, rawOC := range rawPlan.OutputChanges { 84 name := rawOC.Name 85 change, err := changeFromTfplan(rawOC.Change) 86 if err != nil { 87 return nil, fmt.Errorf("invalid plan for output %q: %w", name, err) 88 } 89 90 plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{ 91 // All output values saved in the plan file are root module outputs, 92 // since we don't retain others. (They can be easily recomputed 93 // during apply). 94 Addr: addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance), 95 ChangeSrc: *change, 96 Sensitive: rawOC.Sensitive, 97 }) 98 } 99 100 plan.Checks.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]() 101 for _, rawCRs := range rawPlan.CheckResults { 102 aggr := &states.CheckResultAggregate{} 103 switch rawCRs.Status { 104 case planproto.CheckResults_UNKNOWN: 105 aggr.Status = checks.StatusUnknown 106 case planproto.CheckResults_PASS: 107 aggr.Status = checks.StatusPass 108 case planproto.CheckResults_FAIL: 109 aggr.Status = checks.StatusFail 110 case planproto.CheckResults_ERROR: 111 aggr.Status = checks.StatusError 112 default: 113 return nil, fmt.Errorf("aggregate check results for %s have unsupported status %#v", rawCRs.ConfigAddr, rawCRs.Status) 114 } 115 116 var objKind addrs.CheckableKind 117 switch rawCRs.Kind { 118 case planproto.CheckResults_RESOURCE: 119 objKind = addrs.CheckableResource 120 case planproto.CheckResults_OUTPUT_VALUE: 121 objKind = addrs.CheckableOutputValue 122 case planproto.CheckResults_CHECK: 123 objKind = addrs.CheckableCheck 124 case planproto.CheckResults_INPUT_VARIABLE: 125 objKind = addrs.CheckableInputVariable 126 default: 127 return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", rawCRs.ConfigAddr, objKind) 128 } 129 130 // Some trickiness here: we only have an address parser for 131 // addrs.Checkable and not for addrs.ConfigCheckable, but that's okay 132 // because once we have an addrs.Checkable we can always derive an 133 // addrs.ConfigCheckable from it, and a ConfigCheckable should always 134 // be the same syntax as a Checkable with no index information and 135 // thus we can reuse the same parser for both here. 136 configAddrProxy, diags := addrs.ParseCheckableStr(objKind, rawCRs.ConfigAddr) 137 if diags.HasErrors() { 138 return nil, diags.Err() 139 } 140 configAddr := configAddrProxy.ConfigCheckable() 141 if configAddr.String() != configAddrProxy.String() { 142 // This is how we catch if the config address included index 143 // information that would be allowed in a Checkable but not 144 // in a ConfigCheckable. 145 return nil, fmt.Errorf("invalid checkable config address %s", rawCRs.ConfigAddr) 146 } 147 148 aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]() 149 for _, rawCR := range rawCRs.Objects { 150 objectAddr, diags := addrs.ParseCheckableStr(objKind, rawCR.ObjectAddr) 151 if diags.HasErrors() { 152 return nil, diags.Err() 153 } 154 if !addrs.Equivalent(objectAddr.ConfigCheckable(), configAddr) { 155 return nil, fmt.Errorf("checkable object %s should not be grouped under %s", objectAddr, configAddr) 156 } 157 158 obj := &states.CheckResultObject{ 159 FailureMessages: rawCR.FailureMessages, 160 } 161 switch rawCR.Status { 162 case planproto.CheckResults_UNKNOWN: 163 obj.Status = checks.StatusUnknown 164 case planproto.CheckResults_PASS: 165 obj.Status = checks.StatusPass 166 case planproto.CheckResults_FAIL: 167 obj.Status = checks.StatusFail 168 case planproto.CheckResults_ERROR: 169 obj.Status = checks.StatusError 170 default: 171 return nil, fmt.Errorf("object check results for %s has unsupported status %#v", rawCR.ObjectAddr, rawCR.Status) 172 } 173 174 aggr.ObjectResults.Put(objectAddr, obj) 175 } 176 // If we ended up with no elements in the map then we'll just nil it, 177 // primarily just to make life easier for our round-trip tests. 178 if aggr.ObjectResults.Len() == 0 { 179 aggr.ObjectResults.Elems = nil 180 } 181 182 plan.Checks.ConfigResults.Put(configAddr, aggr) 183 } 184 // If we ended up with no elements in the map then we'll just nil it, 185 // primarily just to make life easier for our round-trip tests. 186 if plan.Checks.ConfigResults.Len() == 0 { 187 plan.Checks.ConfigResults.Elems = nil 188 } 189 190 for _, rawRC := range rawPlan.ResourceChanges { 191 change, err := resourceChangeFromTfplan(rawRC) 192 if err != nil { 193 // errors from resourceChangeFromTfplan already include context 194 return nil, err 195 } 196 197 plan.Changes.Resources = append(plan.Changes.Resources, change) 198 } 199 200 for _, rawRC := range rawPlan.ResourceDrift { 201 change, err := resourceChangeFromTfplan(rawRC) 202 if err != nil { 203 // errors from resourceChangeFromTfplan already include context 204 return nil, err 205 } 206 207 plan.DriftedResources = append(plan.DriftedResources, change) 208 } 209 210 for _, rawRA := range rawPlan.RelevantAttributes { 211 ra, err := resourceAttrFromTfplan(rawRA) 212 if err != nil { 213 return nil, err 214 } 215 plan.RelevantAttributes = append(plan.RelevantAttributes, ra) 216 } 217 218 for _, rawTargetAddr := range rawPlan.TargetAddrs { 219 target, diags := addrs.ParseTargetStr(rawTargetAddr) 220 if diags.HasErrors() { 221 return nil, fmt.Errorf("plan contains invalid target address %q: %w", target, diags.Err()) 222 } 223 plan.TargetAddrs = append(plan.TargetAddrs, target.Subject) 224 } 225 226 for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs { 227 addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr) 228 if diags.HasErrors() { 229 return nil, fmt.Errorf("plan contains invalid force-replace address %q: %w", addr, diags.Err()) 230 } 231 plan.ForceReplaceAddrs = append(plan.ForceReplaceAddrs, addr) 232 } 233 234 for name, rawVal := range rawPlan.Variables { 235 val, err := valueFromTfplan(rawVal) 236 if err != nil { 237 return nil, fmt.Errorf("invalid value for input variable %q: %w", name, err) 238 } 239 plan.VariableValues[name] = val 240 } 241 242 if rawBackend := rawPlan.Backend; rawBackend == nil { 243 return nil, fmt.Errorf("plan file has no backend settings; backend settings are required") 244 } else { 245 config, err := valueFromTfplan(rawBackend.Config) 246 if err != nil { 247 return nil, fmt.Errorf("plan file has invalid backend configuration: %w", err) 248 } 249 plan.Backend = plans.Backend{ 250 Type: rawBackend.Type, 251 Config: config, 252 Workspace: rawBackend.Workspace, 253 } 254 } 255 256 if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil { 257 return nil, fmt.Errorf("invalid value for timestamp %s: %w", rawPlan.Timestamp, err) 258 } 259 260 return plan, nil 261 } 262 263 func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { 264 if rawChange == nil { 265 // Should never happen in practice, since protobuf can't represent 266 // a nil value in a list. 267 return nil, fmt.Errorf("resource change object is absent") 268 } 269 270 ret := &plans.ResourceInstanceChangeSrc{} 271 272 if rawChange.Addr == "" { 273 // If "Addr" isn't populated then seems likely that this is a plan 274 // file created by an earlier version of OpenTofu, which had the 275 // same information spread over various other fields: 276 // ModulePath, Mode, Name, Type, and InstanceKey. 277 return nil, fmt.Errorf("no instance address for resource instance change; perhaps this plan was created by a different version of OpenTofu or a different version of Terraform?") 278 } 279 280 instAddr, diags := addrs.ParseAbsResourceInstanceStr(rawChange.Addr) 281 if diags.HasErrors() { 282 return nil, fmt.Errorf("invalid resource instance address %q: %w", rawChange.Addr, diags.Err()) 283 } 284 prevRunAddr := instAddr 285 if rawChange.PrevRunAddr != "" { 286 prevRunAddr, diags = addrs.ParseAbsResourceInstanceStr(rawChange.PrevRunAddr) 287 if diags.HasErrors() { 288 return nil, fmt.Errorf("invalid resource instance previous run address %q: %w", rawChange.PrevRunAddr, diags.Err()) 289 } 290 } 291 292 providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider) 293 if diags.HasErrors() { 294 return nil, diags.Err() 295 } 296 ret.ProviderAddr = providerAddr 297 298 ret.Addr = instAddr 299 ret.PrevRunAddr = prevRunAddr 300 301 if rawChange.DeposedKey != "" { 302 if len(rawChange.DeposedKey) != 8 { 303 return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey) 304 } 305 ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey) 306 } 307 308 ret.RequiredReplace = cty.NewPathSet() 309 for _, p := range rawChange.RequiredReplace { 310 path, err := pathFromTfplan(p) 311 if err != nil { 312 return nil, fmt.Errorf("invalid path in required replace: %w", err) 313 } 314 ret.RequiredReplace.Add(path) 315 } 316 317 change, err := changeFromTfplan(rawChange.Change) 318 if err != nil { 319 return nil, fmt.Errorf("invalid plan for resource %s: %w", ret.Addr, err) 320 } 321 322 ret.ChangeSrc = *change 323 324 switch rawChange.ActionReason { 325 case planproto.ResourceInstanceActionReason_NONE: 326 ret.ActionReason = plans.ResourceInstanceChangeNoReason 327 case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE: 328 ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate 329 case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED: 330 ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted 331 case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST: 332 ret.ActionReason = plans.ResourceInstanceReplaceByRequest 333 case planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS: 334 ret.ActionReason = plans.ResourceInstanceReplaceByTriggers 335 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG: 336 ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoResourceConfig 337 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION: 338 ret.ActionReason = plans.ResourceInstanceDeleteBecauseWrongRepetition 339 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX: 340 ret.ActionReason = plans.ResourceInstanceDeleteBecauseCountIndex 341 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY: 342 ret.ActionReason = plans.ResourceInstanceDeleteBecauseEachKey 343 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE: 344 ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoModule 345 case planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN: 346 ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown 347 case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING: 348 ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending 349 case planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED: 350 ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested 351 case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET: 352 ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget 353 default: 354 return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) 355 } 356 357 if len(rawChange.Private) != 0 { 358 ret.Private = rawChange.Private 359 } 360 361 return ret, nil 362 } 363 364 func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { 365 if rawChange == nil { 366 return nil, fmt.Errorf("change object is absent") 367 } 368 369 ret := &plans.ChangeSrc{} 370 371 // -1 indicates that there is no index. We'll customize these below 372 // depending on the change action, and then decode. 373 beforeIdx, afterIdx := -1, -1 374 375 switch rawChange.Action { 376 case planproto.Action_NOOP: 377 ret.Action = plans.NoOp 378 beforeIdx = 0 379 afterIdx = 0 380 case planproto.Action_CREATE: 381 ret.Action = plans.Create 382 afterIdx = 0 383 case planproto.Action_READ: 384 ret.Action = plans.Read 385 beforeIdx = 0 386 afterIdx = 1 387 case planproto.Action_UPDATE: 388 ret.Action = plans.Update 389 beforeIdx = 0 390 afterIdx = 1 391 case planproto.Action_DELETE: 392 ret.Action = plans.Delete 393 beforeIdx = 0 394 case planproto.Action_FORGET: 395 ret.Action = plans.Forget 396 beforeIdx = 0 397 case planproto.Action_CREATE_THEN_DELETE: 398 ret.Action = plans.CreateThenDelete 399 beforeIdx = 0 400 afterIdx = 1 401 case planproto.Action_DELETE_THEN_CREATE: 402 ret.Action = plans.DeleteThenCreate 403 beforeIdx = 0 404 afterIdx = 1 405 default: 406 return nil, fmt.Errorf("invalid change action %s", rawChange.Action) 407 } 408 409 if beforeIdx != -1 { 410 if l := len(rawChange.Values); l <= beforeIdx { 411 return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) 412 } 413 var err error 414 ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx]) 415 if err != nil { 416 return nil, fmt.Errorf("invalid \"before\" value: %w", err) 417 } 418 if ret.Before == nil { 419 return nil, fmt.Errorf("missing \"before\" value: %w", err) 420 } 421 } 422 if afterIdx != -1 { 423 if l := len(rawChange.Values); l <= afterIdx { 424 return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) 425 } 426 var err error 427 ret.After, err = valueFromTfplan(rawChange.Values[afterIdx]) 428 if err != nil { 429 return nil, fmt.Errorf("invalid \"after\" value: %w", err) 430 } 431 if ret.After == nil { 432 return nil, fmt.Errorf("missing \"after\" value: %w", err) 433 } 434 } 435 436 if rawChange.Importing != nil { 437 ret.Importing = &plans.ImportingSrc{ 438 ID: rawChange.Importing.Id, 439 } 440 } 441 ret.GeneratedConfig = rawChange.GeneratedConfig 442 443 sensitive := cty.NewValueMarks(marks.Sensitive) 444 beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive) 445 if err != nil { 446 return nil, fmt.Errorf("failed to decode before sensitive paths: %w", err) 447 } 448 afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive) 449 if err != nil { 450 return nil, fmt.Errorf("failed to decode after sensitive paths: %w", err) 451 } 452 if len(beforeValMarks) > 0 { 453 ret.BeforeValMarks = beforeValMarks 454 } 455 if len(afterValMarks) > 0 { 456 ret.AfterValMarks = afterValMarks 457 } 458 459 return ret, nil 460 } 461 462 func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) { 463 if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf 464 return nil, fmt.Errorf("dynamic value does not have msgpack serialization") 465 } 466 467 return plans.DynamicValue(rawV.Msgpack), nil 468 } 469 470 // writeTfplan serializes the given plan into the protobuf-based format used 471 // for the "tfplan" portion of a plan file. 472 func writeTfplan(plan *plans.Plan, w io.Writer) error { 473 if plan == nil { 474 return fmt.Errorf("cannot write plan file for nil plan") 475 } 476 if plan.Changes == nil { 477 return fmt.Errorf("cannot write plan file with nil changeset") 478 } 479 480 rawPlan := &planproto.Plan{ 481 Version: tfplanFormatVersion, 482 TerraformVersion: version.String(), 483 484 Variables: map[string]*planproto.DynamicValue{}, 485 OutputChanges: []*planproto.OutputChange{}, 486 CheckResults: []*planproto.CheckResults{}, 487 ResourceChanges: []*planproto.ResourceInstanceChange{}, 488 ResourceDrift: []*planproto.ResourceInstanceChange{}, 489 } 490 491 rawPlan.Errored = plan.Errored 492 493 switch plan.UIMode { 494 case plans.NormalMode: 495 rawPlan.UiMode = planproto.Mode_NORMAL 496 case plans.DestroyMode: 497 rawPlan.UiMode = planproto.Mode_DESTROY 498 case plans.RefreshOnlyMode: 499 rawPlan.UiMode = planproto.Mode_REFRESH_ONLY 500 default: 501 return fmt.Errorf("plan has unsupported mode %s", plan.UIMode) 502 } 503 504 for _, oc := range plan.Changes.Outputs { 505 // When serializing a plan we only retain the root outputs, since 506 // changes to these are externally-visible side effects (e.g. via 507 // terraform_remote_state). 508 if !oc.Addr.Module.IsRoot() { 509 continue 510 } 511 512 name := oc.Addr.OutputValue.Name 513 514 // Writing outputs as cty.DynamicPseudoType forces the stored values 515 // to also contain dynamic type information, so we can recover the 516 // original type when we read the values back in readTFPlan. 517 protoChange, err := changeToTfplan(&oc.ChangeSrc) 518 if err != nil { 519 return fmt.Errorf("cannot write output value %q: %w", name, err) 520 } 521 522 rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{ 523 Name: name, 524 Change: protoChange, 525 Sensitive: oc.Sensitive, 526 }) 527 } 528 529 if plan.Checks != nil { 530 for _, configElem := range plan.Checks.ConfigResults.Elems { 531 crs := configElem.Value 532 pcrs := &planproto.CheckResults{ 533 ConfigAddr: configElem.Key.String(), 534 } 535 switch crs.Status { 536 case checks.StatusUnknown: 537 pcrs.Status = planproto.CheckResults_UNKNOWN 538 case checks.StatusPass: 539 pcrs.Status = planproto.CheckResults_PASS 540 case checks.StatusFail: 541 pcrs.Status = planproto.CheckResults_FAIL 542 case checks.StatusError: 543 pcrs.Status = planproto.CheckResults_ERROR 544 default: 545 return fmt.Errorf("checkable configuration %s has unsupported aggregate status %s", configElem.Key, crs.Status) 546 } 547 switch kind := configElem.Key.CheckableKind(); kind { 548 case addrs.CheckableResource: 549 pcrs.Kind = planproto.CheckResults_RESOURCE 550 case addrs.CheckableOutputValue: 551 pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE 552 case addrs.CheckableCheck: 553 pcrs.Kind = planproto.CheckResults_CHECK 554 case addrs.CheckableInputVariable: 555 pcrs.Kind = planproto.CheckResults_INPUT_VARIABLE 556 default: 557 return fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind) 558 } 559 560 for _, objectElem := range configElem.Value.ObjectResults.Elems { 561 cr := objectElem.Value 562 pcr := &planproto.CheckResults_ObjectResult{ 563 ObjectAddr: objectElem.Key.String(), 564 FailureMessages: objectElem.Value.FailureMessages, 565 } 566 switch cr.Status { 567 case checks.StatusUnknown: 568 pcr.Status = planproto.CheckResults_UNKNOWN 569 case checks.StatusPass: 570 pcr.Status = planproto.CheckResults_PASS 571 case checks.StatusFail: 572 pcr.Status = planproto.CheckResults_FAIL 573 case checks.StatusError: 574 pcr.Status = planproto.CheckResults_ERROR 575 default: 576 return fmt.Errorf("checkable object %s has unsupported status %s", objectElem.Key, crs.Status) 577 } 578 pcrs.Objects = append(pcrs.Objects, pcr) 579 } 580 581 rawPlan.CheckResults = append(rawPlan.CheckResults, pcrs) 582 } 583 } 584 585 for _, rc := range plan.Changes.Resources { 586 rawRC, err := resourceChangeToTfplan(rc) 587 if err != nil { 588 return err 589 } 590 rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC) 591 } 592 593 for _, rc := range plan.DriftedResources { 594 rawRC, err := resourceChangeToTfplan(rc) 595 if err != nil { 596 return err 597 } 598 rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC) 599 } 600 601 for _, ra := range plan.RelevantAttributes { 602 rawRA, err := resourceAttrToTfplan(ra) 603 if err != nil { 604 return err 605 } 606 rawPlan.RelevantAttributes = append(rawPlan.RelevantAttributes, rawRA) 607 } 608 609 for _, targetAddr := range plan.TargetAddrs { 610 rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String()) 611 } 612 613 for _, replaceAddr := range plan.ForceReplaceAddrs { 614 rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String()) 615 } 616 617 for name, val := range plan.VariableValues { 618 rawPlan.Variables[name] = valueToTfplan(val) 619 } 620 621 if plan.Backend.Type == "" || plan.Backend.Config == nil { 622 // This suggests a bug in the code that created the plan, since it 623 // ought to always have a backend populated, even if it's the default 624 // "local" backend with a local state file. 625 return fmt.Errorf("plan does not have a backend configuration") 626 } 627 628 rawPlan.Backend = &planproto.Backend{ 629 Type: plan.Backend.Type, 630 Config: valueToTfplan(plan.Backend.Config), 631 Workspace: plan.Backend.Workspace, 632 } 633 634 rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339) 635 636 src, err := proto.Marshal(rawPlan) 637 if err != nil { 638 return fmt.Errorf("serialization error: %w", err) 639 } 640 641 _, err = w.Write(src) 642 if err != nil { 643 return fmt.Errorf("failed to write plan to plan file: %w", err) 644 } 645 646 return nil 647 } 648 649 func resourceAttrToTfplan(ra globalref.ResourceAttr) (*planproto.PlanResourceAttr, error) { 650 res := &planproto.PlanResourceAttr{} 651 652 res.Resource = ra.Resource.String() 653 attr, err := pathToTfplan(ra.Attr) 654 if err != nil { 655 return res, err 656 } 657 res.Attr = attr 658 return res, nil 659 } 660 661 func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceAttr, error) { 662 var res globalref.ResourceAttr 663 if ra.Resource == "" { 664 return res, fmt.Errorf("missing resource address from relevant attribute") 665 } 666 667 instAddr, diags := addrs.ParseAbsResourceInstanceStr(ra.Resource) 668 if diags.HasErrors() { 669 return res, fmt.Errorf("invalid resource instance address %q in relevant attributes: %w", ra.Resource, diags.Err()) 670 } 671 672 res.Resource = instAddr 673 path, err := pathFromTfplan(ra.Attr) 674 if err != nil { 675 return res, fmt.Errorf("invalid path in %q relevant attribute: %w", res.Resource, err) 676 } 677 678 res.Attr = path 679 return res, nil 680 } 681 682 func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { 683 ret := &planproto.ResourceInstanceChange{} 684 685 if change.PrevRunAddr.Resource.Resource.Type == "" { 686 // Suggests that an old caller wasn't yet updated to populate this 687 // properly. All code that generates plans should populate this field, 688 // even if it's just to write in the same value as in change.Addr. 689 change.PrevRunAddr = change.Addr 690 } 691 692 ret.Addr = change.Addr.String() 693 ret.PrevRunAddr = change.PrevRunAddr.String() 694 if ret.PrevRunAddr == ret.Addr { 695 // In the on-disk format we leave PrevRunAddr unpopulated in the common 696 // case where it's the same as Addr, and then fill it back in again on 697 // read. 698 ret.PrevRunAddr = "" 699 } 700 701 ret.DeposedKey = string(change.DeposedKey) 702 ret.Provider = change.ProviderAddr.String() 703 704 requiredReplace := change.RequiredReplace.List() 705 ret.RequiredReplace = make([]*planproto.Path, 0, len(requiredReplace)) 706 for _, p := range requiredReplace { 707 path, err := pathToTfplan(p) 708 if err != nil { 709 return nil, fmt.Errorf("invalid path in required replace: %w", err) 710 } 711 ret.RequiredReplace = append(ret.RequiredReplace, path) 712 } 713 714 valChange, err := changeToTfplan(&change.ChangeSrc) 715 if err != nil { 716 return nil, fmt.Errorf("failed to serialize resource %s change: %w", change.Addr, err) 717 } 718 ret.Change = valChange 719 720 switch change.ActionReason { 721 case plans.ResourceInstanceChangeNoReason: 722 ret.ActionReason = planproto.ResourceInstanceActionReason_NONE 723 case plans.ResourceInstanceReplaceBecauseCannotUpdate: 724 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE 725 case plans.ResourceInstanceReplaceBecauseTainted: 726 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED 727 case plans.ResourceInstanceReplaceByRequest: 728 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST 729 case plans.ResourceInstanceReplaceByTriggers: 730 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_TRIGGERS 731 case plans.ResourceInstanceDeleteBecauseNoResourceConfig: 732 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG 733 case plans.ResourceInstanceDeleteBecauseWrongRepetition: 734 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION 735 case plans.ResourceInstanceDeleteBecauseCountIndex: 736 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX 737 case plans.ResourceInstanceDeleteBecauseEachKey: 738 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY 739 case plans.ResourceInstanceDeleteBecauseNoModule: 740 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE 741 case plans.ResourceInstanceReadBecauseConfigUnknown: 742 ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN 743 case plans.ResourceInstanceReadBecauseDependencyPending: 744 ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING 745 case plans.ResourceInstanceReadBecauseCheckNested: 746 ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED 747 case plans.ResourceInstanceDeleteBecauseNoMoveTarget: 748 ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET 749 default: 750 return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason) 751 } 752 753 if len(change.Private) > 0 { 754 ret.Private = change.Private 755 } 756 757 return ret, nil 758 } 759 760 func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { 761 ret := &planproto.Change{} 762 763 before := valueToTfplan(change.Before) 764 after := valueToTfplan(change.After) 765 766 beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks) 767 if err != nil { 768 return nil, err 769 } 770 afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks) 771 if err != nil { 772 return nil, err 773 } 774 ret.BeforeSensitivePaths = beforeSensitivePaths 775 ret.AfterSensitivePaths = afterSensitivePaths 776 777 if change.Importing != nil { 778 ret.Importing = &planproto.Importing{ 779 Id: change.Importing.ID, 780 } 781 782 } 783 ret.GeneratedConfig = change.GeneratedConfig 784 785 switch change.Action { 786 case plans.NoOp: 787 ret.Action = planproto.Action_NOOP 788 ret.Values = []*planproto.DynamicValue{before} // before and after should be identical 789 case plans.Create: 790 ret.Action = planproto.Action_CREATE 791 ret.Values = []*planproto.DynamicValue{after} 792 case plans.Read: 793 ret.Action = planproto.Action_READ 794 ret.Values = []*planproto.DynamicValue{before, after} 795 case plans.Update: 796 ret.Action = planproto.Action_UPDATE 797 ret.Values = []*planproto.DynamicValue{before, after} 798 case plans.Delete: 799 ret.Action = planproto.Action_DELETE 800 ret.Values = []*planproto.DynamicValue{before} 801 case plans.Forget: 802 ret.Action = planproto.Action_FORGET 803 ret.Values = []*planproto.DynamicValue{before} 804 case plans.DeleteThenCreate: 805 ret.Action = planproto.Action_DELETE_THEN_CREATE 806 ret.Values = []*planproto.DynamicValue{before, after} 807 case plans.CreateThenDelete: 808 ret.Action = planproto.Action_CREATE_THEN_DELETE 809 ret.Values = []*planproto.DynamicValue{before, after} 810 default: 811 return nil, fmt.Errorf("invalid change action %s", change.Action) 812 } 813 814 return ret, nil 815 } 816 817 func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue { 818 if val == nil { 819 // protobuf can't represent nil, so we'll represent it as a 820 // DynamicValue that has no serializations at all. 821 return &planproto.DynamicValue{} 822 } 823 return &planproto.DynamicValue{ 824 Msgpack: []byte(val), 825 } 826 } 827 828 func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) { 829 ret := make([]cty.PathValueMarks, 0, len(paths)) 830 for _, p := range paths { 831 path, err := pathFromTfplan(p) 832 if err != nil { 833 return nil, err 834 } 835 ret = append(ret, cty.PathValueMarks{ 836 Path: path, 837 Marks: marks, 838 }) 839 } 840 return ret, nil 841 } 842 843 func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) { 844 ret := make([]*planproto.Path, 0, len(pvm)) 845 for _, p := range pvm { 846 path, err := pathToTfplan(p.Path) 847 if err != nil { 848 return nil, err 849 } 850 ret = append(ret, path) 851 } 852 return ret, nil 853 } 854 855 func pathFromTfplan(path *planproto.Path) (cty.Path, error) { 856 ret := make([]cty.PathStep, 0, len(path.Steps)) 857 for _, step := range path.Steps { 858 switch s := step.Selector.(type) { 859 case *planproto.Path_Step_ElementKey: 860 dynamicVal, err := valueFromTfplan(s.ElementKey) 861 if err != nil { 862 return nil, fmt.Errorf("error decoding path index step: %w", err) 863 } 864 ty, err := dynamicVal.ImpliedType() 865 if err != nil { 866 return nil, fmt.Errorf("error determining path index type: %w", err) 867 } 868 val, err := dynamicVal.Decode(ty) 869 if err != nil { 870 return nil, fmt.Errorf("error decoding path index value: %w", err) 871 } 872 ret = append(ret, cty.IndexStep{Key: val}) 873 case *planproto.Path_Step_AttributeName: 874 ret = append(ret, cty.GetAttrStep{Name: s.AttributeName}) 875 default: 876 return nil, fmt.Errorf("Unsupported path step %t", step.Selector) 877 } 878 } 879 return ret, nil 880 } 881 882 func pathToTfplan(path cty.Path) (*planproto.Path, error) { 883 steps := make([]*planproto.Path_Step, 0, len(path)) 884 for _, step := range path { 885 switch s := step.(type) { 886 case cty.IndexStep: 887 value, err := plans.NewDynamicValue(s.Key, s.Key.Type()) 888 if err != nil { 889 return nil, fmt.Errorf("Error encoding path step: %w", err) 890 } 891 steps = append(steps, &planproto.Path_Step{ 892 Selector: &planproto.Path_Step_ElementKey{ 893 ElementKey: valueToTfplan(value), 894 }, 895 }) 896 case cty.GetAttrStep: 897 steps = append(steps, &planproto.Path_Step{ 898 Selector: &planproto.Path_Step_AttributeName{ 899 AttributeName: s.Name, 900 }, 901 }) 902 default: 903 return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) 904 } 905 } 906 return &planproto.Path{ 907 Steps: steps, 908 }, nil 909 }