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