github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/plans/planfile/tfplan.go (about) 1 package planfile 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 8 "google.golang.org/protobuf/proto" 9 10 "github.com/iaas-resource-provision/iaas-rpc/internal/addrs" 11 "github.com/iaas-resource-provision/iaas-rpc/internal/plans" 12 "github.com/iaas-resource-provision/iaas-rpc/internal/plans/internal/planproto" 13 "github.com/iaas-resource-provision/iaas-rpc/internal/states" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags" 15 "github.com/iaas-resource-provision/iaas-rpc/version" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 const tfplanFormatVersion = 3 20 const tfplanFilename = "tfplan" 21 22 // --------------------------------------------------------------------------- 23 // This file deals with the internal structure of the "tfplan" sub-file within 24 // the plan file format. It's all private API, wrapped by methods defined 25 // elsewhere. This is the only file that should import the 26 // ../internal/planproto package, which contains the ugly stubs generated 27 // by the protobuf compiler. 28 // --------------------------------------------------------------------------- 29 30 // readTfplan reads a protobuf-encoded description from the plan portion of 31 // a plan file, which is stored in a special file in the archive called 32 // "tfplan". 33 func readTfplan(r io.Reader) (*plans.Plan, error) { 34 src, err := ioutil.ReadAll(r) 35 if err != nil { 36 return nil, err 37 } 38 39 var rawPlan planproto.Plan 40 err = proto.Unmarshal(src, &rawPlan) 41 if err != nil { 42 return nil, fmt.Errorf("parse error: %s", err) 43 } 44 45 if rawPlan.Version != tfplanFormatVersion { 46 return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion) 47 } 48 49 if rawPlan.TerraformVersion != version.String() { 50 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()) 51 } 52 53 plan := &plans.Plan{ 54 VariableValues: map[string]plans.DynamicValue{}, 55 Changes: &plans.Changes{ 56 Outputs: []*plans.OutputChangeSrc{}, 57 Resources: []*plans.ResourceInstanceChangeSrc{}, 58 }, 59 60 ProviderSHA256s: map[string][]byte{}, 61 } 62 63 switch rawPlan.UiMode { 64 case planproto.Mode_NORMAL: 65 plan.UIMode = plans.NormalMode 66 case planproto.Mode_DESTROY: 67 plan.UIMode = plans.DestroyMode 68 case planproto.Mode_REFRESH_ONLY: 69 plan.UIMode = plans.RefreshOnlyMode 70 default: 71 return nil, fmt.Errorf("plan has invalid mode %s", rawPlan.UiMode) 72 } 73 74 for _, rawOC := range rawPlan.OutputChanges { 75 name := rawOC.Name 76 change, err := changeFromTfplan(rawOC.Change) 77 if err != nil { 78 return nil, fmt.Errorf("invalid plan for output %q: %s", name, err) 79 } 80 81 plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{ 82 // All output values saved in the plan file are root module outputs, 83 // since we don't retain others. (They can be easily recomputed 84 // during apply). 85 Addr: addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance), 86 ChangeSrc: *change, 87 Sensitive: rawOC.Sensitive, 88 }) 89 } 90 91 for _, rawRC := range rawPlan.ResourceChanges { 92 change, err := resourceChangeFromTfplan(rawRC) 93 if err != nil { 94 // errors from resourceChangeFromTfplan already include context 95 return nil, err 96 } 97 98 plan.Changes.Resources = append(plan.Changes.Resources, change) 99 } 100 101 for _, rawTargetAddr := range rawPlan.TargetAddrs { 102 target, diags := addrs.ParseTargetStr(rawTargetAddr) 103 if diags.HasErrors() { 104 return nil, fmt.Errorf("plan contains invalid target address %q: %s", target, diags.Err()) 105 } 106 plan.TargetAddrs = append(plan.TargetAddrs, target.Subject) 107 } 108 109 for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs { 110 addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr) 111 if diags.HasErrors() { 112 return nil, fmt.Errorf("plan contains invalid force-replace address %q: %s", addr, diags.Err()) 113 } 114 plan.ForceReplaceAddrs = append(plan.ForceReplaceAddrs, addr) 115 } 116 117 for name, rawHashObj := range rawPlan.ProviderHashes { 118 if len(rawHashObj.Sha256) == 0 { 119 return nil, fmt.Errorf("no SHA256 hash for provider %q plugin", name) 120 } 121 122 plan.ProviderSHA256s[name] = rawHashObj.Sha256 123 } 124 125 for name, rawVal := range rawPlan.Variables { 126 val, err := valueFromTfplan(rawVal) 127 if err != nil { 128 return nil, fmt.Errorf("invalid value for input variable %q: %s", name, err) 129 } 130 plan.VariableValues[name] = val 131 } 132 133 if rawBackend := rawPlan.Backend; rawBackend == nil { 134 return nil, fmt.Errorf("plan file has no backend settings; backend settings are required") 135 } else { 136 config, err := valueFromTfplan(rawBackend.Config) 137 if err != nil { 138 return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err) 139 } 140 plan.Backend = plans.Backend{ 141 Type: rawBackend.Type, 142 Config: config, 143 Workspace: rawBackend.Workspace, 144 } 145 } 146 147 return plan, nil 148 } 149 150 func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { 151 if rawChange == nil { 152 // Should never happen in practice, since protobuf can't represent 153 // a nil value in a list. 154 return nil, fmt.Errorf("resource change object is absent") 155 } 156 157 ret := &plans.ResourceInstanceChangeSrc{} 158 159 moduleAddr := addrs.RootModuleInstance 160 if rawChange.ModulePath != "" { 161 var diags tfdiags.Diagnostics 162 moduleAddr, diags = addrs.ParseModuleInstanceStr(rawChange.ModulePath) 163 if diags.HasErrors() { 164 return nil, diags.Err() 165 } 166 } 167 168 providerAddr, diags := addrs.ParseAbsProviderConfigStr(rawChange.Provider) 169 if diags.HasErrors() { 170 return nil, diags.Err() 171 } 172 ret.ProviderAddr = providerAddr 173 174 var mode addrs.ResourceMode 175 switch rawChange.Mode { 176 case planproto.ResourceInstanceChange_managed: 177 mode = addrs.ManagedResourceMode 178 case planproto.ResourceInstanceChange_data: 179 mode = addrs.DataResourceMode 180 default: 181 return nil, fmt.Errorf("resource has invalid mode %s", rawChange.Mode) 182 } 183 184 typeName := rawChange.Type 185 name := rawChange.Name 186 187 resAddr := addrs.Resource{ 188 Mode: mode, 189 Type: typeName, 190 Name: name, 191 } 192 193 var instKey addrs.InstanceKey 194 switch rawTk := rawChange.InstanceKey.(type) { 195 case nil: 196 case *planproto.ResourceInstanceChange_Int: 197 instKey = addrs.IntKey(rawTk.Int) 198 case *planproto.ResourceInstanceChange_Str: 199 instKey = addrs.StringKey(rawTk.Str) 200 default: 201 return nil, fmt.Errorf("instance of %s has invalid key type %T", resAddr.Absolute(moduleAddr), rawChange.InstanceKey) 202 } 203 204 ret.Addr = resAddr.Instance(instKey).Absolute(moduleAddr) 205 206 if rawChange.DeposedKey != "" { 207 if len(rawChange.DeposedKey) != 8 { 208 return nil, fmt.Errorf("deposed object for %s has invalid deposed key %q", ret.Addr, rawChange.DeposedKey) 209 } 210 ret.DeposedKey = states.DeposedKey(rawChange.DeposedKey) 211 } 212 213 ret.RequiredReplace = cty.NewPathSet() 214 for _, p := range rawChange.RequiredReplace { 215 path, err := pathFromTfplan(p) 216 if err != nil { 217 return nil, fmt.Errorf("invalid path in required replace: %s", err) 218 } 219 ret.RequiredReplace.Add(path) 220 } 221 222 change, err := changeFromTfplan(rawChange.Change) 223 if err != nil { 224 return nil, fmt.Errorf("invalid plan for resource %s: %s", ret.Addr, err) 225 } 226 227 ret.ChangeSrc = *change 228 229 switch rawChange.ActionReason { 230 case planproto.ResourceInstanceActionReason_NONE: 231 ret.ActionReason = plans.ResourceInstanceChangeNoReason 232 case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE: 233 ret.ActionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate 234 case planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED: 235 ret.ActionReason = plans.ResourceInstanceReplaceBecauseTainted 236 case planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST: 237 ret.ActionReason = plans.ResourceInstanceReplaceByRequest 238 default: 239 return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) 240 } 241 242 if len(rawChange.Private) != 0 { 243 ret.Private = rawChange.Private 244 } 245 246 return ret, nil 247 } 248 249 func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { 250 if rawChange == nil { 251 return nil, fmt.Errorf("change object is absent") 252 } 253 254 ret := &plans.ChangeSrc{} 255 256 // -1 indicates that there is no index. We'll customize these below 257 // depending on the change action, and then decode. 258 beforeIdx, afterIdx := -1, -1 259 260 switch rawChange.Action { 261 case planproto.Action_NOOP: 262 ret.Action = plans.NoOp 263 beforeIdx = 0 264 afterIdx = 0 265 case planproto.Action_CREATE: 266 ret.Action = plans.Create 267 afterIdx = 0 268 case planproto.Action_READ: 269 ret.Action = plans.Read 270 beforeIdx = 0 271 afterIdx = 1 272 case planproto.Action_UPDATE: 273 ret.Action = plans.Update 274 beforeIdx = 0 275 afterIdx = 1 276 case planproto.Action_DELETE: 277 ret.Action = plans.Delete 278 beforeIdx = 0 279 case planproto.Action_CREATE_THEN_DELETE: 280 ret.Action = plans.CreateThenDelete 281 beforeIdx = 0 282 afterIdx = 1 283 case planproto.Action_DELETE_THEN_CREATE: 284 ret.Action = plans.DeleteThenCreate 285 beforeIdx = 0 286 afterIdx = 1 287 default: 288 return nil, fmt.Errorf("invalid change action %s", rawChange.Action) 289 } 290 291 if beforeIdx != -1 { 292 if l := len(rawChange.Values); l <= beforeIdx { 293 return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) 294 } 295 var err error 296 ret.Before, err = valueFromTfplan(rawChange.Values[beforeIdx]) 297 if err != nil { 298 return nil, fmt.Errorf("invalid \"before\" value: %s", err) 299 } 300 if ret.Before == nil { 301 return nil, fmt.Errorf("missing \"before\" value: %s", err) 302 } 303 } 304 if afterIdx != -1 { 305 if l := len(rawChange.Values); l <= afterIdx { 306 return nil, fmt.Errorf("incorrect number of values (%d) for %s change", l, rawChange.Action) 307 } 308 var err error 309 ret.After, err = valueFromTfplan(rawChange.Values[afterIdx]) 310 if err != nil { 311 return nil, fmt.Errorf("invalid \"after\" value: %s", err) 312 } 313 if ret.After == nil { 314 return nil, fmt.Errorf("missing \"after\" value: %s", err) 315 } 316 } 317 318 sensitive := cty.NewValueMarks("sensitive") 319 beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive) 320 if err != nil { 321 return nil, fmt.Errorf("failed to decode before sensitive paths: %s", err) 322 } 323 afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive) 324 if err != nil { 325 return nil, fmt.Errorf("failed to decode after sensitive paths: %s", err) 326 } 327 if len(beforeValMarks) > 0 { 328 ret.BeforeValMarks = beforeValMarks 329 } 330 if len(afterValMarks) > 0 { 331 ret.AfterValMarks = afterValMarks 332 } 333 334 return ret, nil 335 } 336 337 func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) { 338 if len(rawV.Msgpack) == 0 { // len(0) because that's the default value for a "bytes" in protobuf 339 return nil, fmt.Errorf("dynamic value does not have msgpack serialization") 340 } 341 342 return plans.DynamicValue(rawV.Msgpack), nil 343 } 344 345 // writeTfplan serializes the given plan into the protobuf-based format used 346 // for the "tfplan" portion of a plan file. 347 func writeTfplan(plan *plans.Plan, w io.Writer) error { 348 if plan == nil { 349 return fmt.Errorf("cannot write plan file for nil plan") 350 } 351 if plan.Changes == nil { 352 return fmt.Errorf("cannot write plan file with nil changeset") 353 } 354 355 rawPlan := &planproto.Plan{ 356 Version: tfplanFormatVersion, 357 TerraformVersion: version.String(), 358 ProviderHashes: map[string]*planproto.Hash{}, 359 360 Variables: map[string]*planproto.DynamicValue{}, 361 OutputChanges: []*planproto.OutputChange{}, 362 ResourceChanges: []*planproto.ResourceInstanceChange{}, 363 } 364 365 switch plan.UIMode { 366 case plans.NormalMode: 367 rawPlan.UiMode = planproto.Mode_NORMAL 368 case plans.DestroyMode: 369 rawPlan.UiMode = planproto.Mode_DESTROY 370 case plans.RefreshOnlyMode: 371 rawPlan.UiMode = planproto.Mode_REFRESH_ONLY 372 default: 373 return fmt.Errorf("plan has unsupported mode %s", plan.UIMode) 374 } 375 376 for _, oc := range plan.Changes.Outputs { 377 // When serializing a plan we only retain the root outputs, since 378 // changes to these are externally-visible side effects (e.g. via 379 // terraform_remote_state). 380 if !oc.Addr.Module.IsRoot() { 381 continue 382 } 383 384 name := oc.Addr.OutputValue.Name 385 386 // Writing outputs as cty.DynamicPseudoType forces the stored values 387 // to also contain dynamic type information, so we can recover the 388 // original type when we read the values back in readTFPlan. 389 protoChange, err := changeToTfplan(&oc.ChangeSrc) 390 if err != nil { 391 return fmt.Errorf("cannot write output value %q: %s", name, err) 392 } 393 394 rawPlan.OutputChanges = append(rawPlan.OutputChanges, &planproto.OutputChange{ 395 Name: name, 396 Change: protoChange, 397 Sensitive: oc.Sensitive, 398 }) 399 } 400 401 for _, rc := range plan.Changes.Resources { 402 rawRC, err := resourceChangeToTfplan(rc) 403 if err != nil { 404 return err 405 } 406 rawPlan.ResourceChanges = append(rawPlan.ResourceChanges, rawRC) 407 } 408 409 for _, targetAddr := range plan.TargetAddrs { 410 rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String()) 411 } 412 413 for _, replaceAddr := range plan.ForceReplaceAddrs { 414 rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String()) 415 } 416 417 for name, hash := range plan.ProviderSHA256s { 418 rawPlan.ProviderHashes[name] = &planproto.Hash{ 419 Sha256: hash, 420 } 421 } 422 423 for name, val := range plan.VariableValues { 424 rawPlan.Variables[name] = valueToTfplan(val) 425 } 426 427 if plan.Backend.Type == "" || plan.Backend.Config == nil { 428 // This suggests a bug in the code that created the plan, since it 429 // ought to always have a backend populated, even if it's the default 430 // "local" backend with a local state file. 431 return fmt.Errorf("plan does not have a backend configuration") 432 } 433 434 rawPlan.Backend = &planproto.Backend{ 435 Type: plan.Backend.Type, 436 Config: valueToTfplan(plan.Backend.Config), 437 Workspace: plan.Backend.Workspace, 438 } 439 440 src, err := proto.Marshal(rawPlan) 441 if err != nil { 442 return fmt.Errorf("serialization error: %s", err) 443 } 444 445 _, err = w.Write(src) 446 if err != nil { 447 return fmt.Errorf("failed to write plan to plan file: %s", err) 448 } 449 450 return nil 451 } 452 453 func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { 454 ret := &planproto.ResourceInstanceChange{} 455 456 ret.ModulePath = change.Addr.Module.String() 457 458 relAddr := change.Addr.Resource 459 460 switch relAddr.Resource.Mode { 461 case addrs.ManagedResourceMode: 462 ret.Mode = planproto.ResourceInstanceChange_managed 463 case addrs.DataResourceMode: 464 ret.Mode = planproto.ResourceInstanceChange_data 465 default: 466 return nil, fmt.Errorf("resource %s has unsupported mode %s", relAddr, relAddr.Resource.Mode) 467 } 468 469 ret.Type = relAddr.Resource.Type 470 ret.Name = relAddr.Resource.Name 471 472 switch tk := relAddr.Key.(type) { 473 case nil: 474 // Nothing to do, then. 475 case addrs.IntKey: 476 ret.InstanceKey = &planproto.ResourceInstanceChange_Int{ 477 Int: int64(tk), 478 } 479 case addrs.StringKey: 480 ret.InstanceKey = &planproto.ResourceInstanceChange_Str{ 481 Str: string(tk), 482 } 483 default: 484 return nil, fmt.Errorf("resource %s has unsupported instance key type %T", relAddr, relAddr.Key) 485 } 486 487 ret.DeposedKey = string(change.DeposedKey) 488 ret.Provider = change.ProviderAddr.String() 489 490 requiredReplace := change.RequiredReplace.List() 491 ret.RequiredReplace = make([]*planproto.Path, 0, len(requiredReplace)) 492 for _, p := range requiredReplace { 493 path, err := pathToTfplan(p) 494 if err != nil { 495 return nil, fmt.Errorf("invalid path in required replace: %s", err) 496 } 497 ret.RequiredReplace = append(ret.RequiredReplace, path) 498 } 499 500 valChange, err := changeToTfplan(&change.ChangeSrc) 501 if err != nil { 502 return nil, fmt.Errorf("failed to serialize resource %s change: %s", relAddr, err) 503 } 504 ret.Change = valChange 505 506 switch change.ActionReason { 507 case plans.ResourceInstanceChangeNoReason: 508 ret.ActionReason = planproto.ResourceInstanceActionReason_NONE 509 case plans.ResourceInstanceReplaceBecauseCannotUpdate: 510 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE 511 case plans.ResourceInstanceReplaceBecauseTainted: 512 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED 513 case plans.ResourceInstanceReplaceByRequest: 514 ret.ActionReason = planproto.ResourceInstanceActionReason_REPLACE_BY_REQUEST 515 default: 516 return nil, fmt.Errorf("resource %s has unsupported action reason %s", relAddr, change.ActionReason) 517 } 518 519 if len(change.Private) > 0 { 520 ret.Private = change.Private 521 } 522 523 return ret, nil 524 } 525 526 func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { 527 ret := &planproto.Change{} 528 529 before := valueToTfplan(change.Before) 530 after := valueToTfplan(change.After) 531 532 beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks) 533 if err != nil { 534 return nil, err 535 } 536 afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks) 537 if err != nil { 538 return nil, err 539 } 540 ret.BeforeSensitivePaths = beforeSensitivePaths 541 ret.AfterSensitivePaths = afterSensitivePaths 542 543 switch change.Action { 544 case plans.NoOp: 545 ret.Action = planproto.Action_NOOP 546 ret.Values = []*planproto.DynamicValue{before} // before and after should be identical 547 case plans.Create: 548 ret.Action = planproto.Action_CREATE 549 ret.Values = []*planproto.DynamicValue{after} 550 case plans.Read: 551 ret.Action = planproto.Action_READ 552 ret.Values = []*planproto.DynamicValue{before, after} 553 case plans.Update: 554 ret.Action = planproto.Action_UPDATE 555 ret.Values = []*planproto.DynamicValue{before, after} 556 case plans.Delete: 557 ret.Action = planproto.Action_DELETE 558 ret.Values = []*planproto.DynamicValue{before} 559 case plans.DeleteThenCreate: 560 ret.Action = planproto.Action_DELETE_THEN_CREATE 561 ret.Values = []*planproto.DynamicValue{before, after} 562 case plans.CreateThenDelete: 563 ret.Action = planproto.Action_CREATE_THEN_DELETE 564 ret.Values = []*planproto.DynamicValue{before, after} 565 default: 566 return nil, fmt.Errorf("invalid change action %s", change.Action) 567 } 568 569 return ret, nil 570 } 571 572 func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue { 573 if val == nil { 574 // protobuf can't represent nil, so we'll represent it as a 575 // DynamicValue that has no serializations at all. 576 return &planproto.DynamicValue{} 577 } 578 return &planproto.DynamicValue{ 579 Msgpack: []byte(val), 580 } 581 } 582 583 func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) { 584 ret := make([]cty.PathValueMarks, 0, len(paths)) 585 for _, p := range paths { 586 path, err := pathFromTfplan(p) 587 if err != nil { 588 return nil, err 589 } 590 ret = append(ret, cty.PathValueMarks{ 591 Path: path, 592 Marks: marks, 593 }) 594 } 595 return ret, nil 596 } 597 598 func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) { 599 ret := make([]*planproto.Path, 0, len(pvm)) 600 for _, p := range pvm { 601 path, err := pathToTfplan(p.Path) 602 if err != nil { 603 return nil, err 604 } 605 ret = append(ret, path) 606 } 607 return ret, nil 608 } 609 610 func pathFromTfplan(path *planproto.Path) (cty.Path, error) { 611 ret := make([]cty.PathStep, 0, len(path.Steps)) 612 for _, step := range path.Steps { 613 switch s := step.Selector.(type) { 614 case *planproto.Path_Step_ElementKey: 615 dynamicVal, err := valueFromTfplan(s.ElementKey) 616 if err != nil { 617 return nil, fmt.Errorf("error decoding path index step: %s", err) 618 } 619 ty, err := dynamicVal.ImpliedType() 620 if err != nil { 621 return nil, fmt.Errorf("error determining path index type: %s", err) 622 } 623 val, err := dynamicVal.Decode(ty) 624 if err != nil { 625 return nil, fmt.Errorf("error decoding path index value: %s", err) 626 } 627 ret = append(ret, cty.IndexStep{Key: val}) 628 case *planproto.Path_Step_AttributeName: 629 ret = append(ret, cty.GetAttrStep{Name: s.AttributeName}) 630 default: 631 return nil, fmt.Errorf("Unsupported path step %t", step.Selector) 632 } 633 } 634 return ret, nil 635 } 636 637 func pathToTfplan(path cty.Path) (*planproto.Path, error) { 638 steps := make([]*planproto.Path_Step, 0, len(path)) 639 for _, step := range path { 640 switch s := step.(type) { 641 case cty.IndexStep: 642 value, err := plans.NewDynamicValue(s.Key, s.Key.Type()) 643 if err != nil { 644 return nil, fmt.Errorf("Error encoding path step: %s", err) 645 } 646 steps = append(steps, &planproto.Path_Step{ 647 Selector: &planproto.Path_Step_ElementKey{ 648 ElementKey: valueToTfplan(value), 649 }, 650 }) 651 case cty.GetAttrStep: 652 steps = append(steps, &planproto.Path_Step{ 653 Selector: &planproto.Path_Step_AttributeName{ 654 AttributeName: s.Name, 655 }, 656 }) 657 default: 658 return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) 659 } 660 } 661 return &planproto.Path{ 662 Steps: steps, 663 }, nil 664 }