1package jsonplan 2 3import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 8 "github.com/zclconf/go-cty/cty" 9 ctyjson "github.com/zclconf/go-cty/cty/json" 10 11 "github.com/hashicorp/terraform/internal/addrs" 12 "github.com/hashicorp/terraform/internal/command/jsonconfig" 13 "github.com/hashicorp/terraform/internal/command/jsonstate" 14 "github.com/hashicorp/terraform/internal/configs" 15 "github.com/hashicorp/terraform/internal/plans" 16 "github.com/hashicorp/terraform/internal/states" 17 "github.com/hashicorp/terraform/internal/states/statefile" 18 "github.com/hashicorp/terraform/internal/terraform" 19 "github.com/hashicorp/terraform/version" 20) 21 22// FormatVersion represents the version of the json format and will be 23// incremented for any change to this format that requires changes to a 24// consuming parser. 25const FormatVersion = "0.2" 26 27// Plan is the top-level representation of the json format of a plan. It includes 28// the complete config and current state. 29type plan struct { 30 FormatVersion string `json:"format_version,omitempty"` 31 TerraformVersion string `json:"terraform_version,omitempty"` 32 Variables variables `json:"variables,omitempty"` 33 PlannedValues stateValues `json:"planned_values,omitempty"` 34 // ResourceDrift and ResourceChanges are sorted in a user-friendly order 35 // that is undefined at this time, but consistent. 36 ResourceDrift []resourceChange `json:"resource_drift,omitempty"` 37 ResourceChanges []resourceChange `json:"resource_changes,omitempty"` 38 OutputChanges map[string]change `json:"output_changes,omitempty"` 39 PriorState json.RawMessage `json:"prior_state,omitempty"` 40 Config json.RawMessage `json:"configuration,omitempty"` 41} 42 43func newPlan() *plan { 44 return &plan{ 45 FormatVersion: FormatVersion, 46 } 47} 48 49// Change is the representation of a proposed change for an object. 50type change struct { 51 // Actions are the actions that will be taken on the object selected by the 52 // properties below. Valid actions values are: 53 // ["no-op"] 54 // ["create"] 55 // ["read"] 56 // ["update"] 57 // ["delete", "create"] 58 // ["create", "delete"] 59 // ["delete"] 60 // The two "replace" actions are represented in this way to allow callers to 61 // e.g. just scan the list for "delete" to recognize all three situations 62 // where the object will be deleted, allowing for any new deletion 63 // combinations that might be added in future. 64 Actions []string `json:"actions,omitempty"` 65 66 // Before and After are representations of the object value both before and 67 // after the action. For ["create"] and ["delete"] actions, either "before" 68 // or "after" is unset (respectively). For ["no-op"], the before and after 69 // values are identical. The "after" value will be incomplete if there are 70 // values within it that won't be known until after apply. 71 Before json.RawMessage `json:"before,omitempty"` 72 After json.RawMessage `json:"after,omitempty"` 73 74 // AfterUnknown is an object value with similar structure to After, but 75 // with all unknown leaf values replaced with true, and all known leaf 76 // values omitted. This can be combined with After to reconstruct a full 77 // value after the action, including values which will only be known after 78 // apply. 79 AfterUnknown json.RawMessage `json:"after_unknown,omitempty"` 80 81 // BeforeSensitive and AfterSensitive are object values with similar 82 // structure to Before and After, but with all sensitive leaf values 83 // replaced with true, and all non-sensitive leaf values omitted. These 84 // objects should be combined with Before and After to prevent accidental 85 // display of sensitive values in user interfaces. 86 BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"` 87 AfterSensitive json.RawMessage `json:"after_sensitive,omitempty"` 88 89 // ReplacePaths is an array of arrays representing a set of paths into the 90 // object value which resulted in the action being "replace". This will be 91 // omitted if the action is not replace, or if no paths caused the 92 // replacement (for example, if the resource was tainted). Each path 93 // consists of one or more steps, each of which will be a number or a 94 // string. 95 ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` 96} 97 98type output struct { 99 Sensitive bool `json:"sensitive"` 100 Value json.RawMessage `json:"value,omitempty"` 101} 102 103// variables is the JSON representation of the variables provided to the current 104// plan. 105type variables map[string]*variable 106 107type variable struct { 108 Value json.RawMessage `json:"value,omitempty"` 109} 110 111// Marshal returns the json encoding of a terraform plan. 112func Marshal( 113 config *configs.Config, 114 p *plans.Plan, 115 sf *statefile.File, 116 schemas *terraform.Schemas, 117) ([]byte, error) { 118 output := newPlan() 119 output.TerraformVersion = version.String() 120 121 err := output.marshalPlanVariables(p.VariableValues, schemas) 122 if err != nil { 123 return nil, fmt.Errorf("error in marshalPlanVariables: %s", err) 124 } 125 126 // output.PlannedValues 127 err = output.marshalPlannedValues(p.Changes, schemas) 128 if err != nil { 129 return nil, fmt.Errorf("error in marshalPlannedValues: %s", err) 130 } 131 132 // output.ResourceDrift 133 err = output.marshalResourceDrift(p.PrevRunState, p.PriorState, schemas) 134 if err != nil { 135 return nil, fmt.Errorf("error in marshalResourceDrift: %s", err) 136 } 137 138 // output.ResourceChanges 139 err = output.marshalResourceChanges(p.Changes, schemas) 140 if err != nil { 141 return nil, fmt.Errorf("error in marshalResourceChanges: %s", err) 142 } 143 144 // output.OutputChanges 145 err = output.marshalOutputChanges(p.Changes) 146 if err != nil { 147 return nil, fmt.Errorf("error in marshaling output changes: %s", err) 148 } 149 150 // output.PriorState 151 if sf != nil && !sf.State.Empty() { 152 output.PriorState, err = jsonstate.Marshal(sf, schemas) 153 if err != nil { 154 return nil, fmt.Errorf("error marshaling prior state: %s", err) 155 } 156 } 157 158 // output.Config 159 output.Config, err = jsonconfig.Marshal(config, schemas) 160 if err != nil { 161 return nil, fmt.Errorf("error marshaling config: %s", err) 162 } 163 164 ret, err := json.Marshal(output) 165 return ret, err 166} 167 168func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, schemas *terraform.Schemas) error { 169 if len(vars) == 0 { 170 return nil 171 } 172 173 p.Variables = make(variables, len(vars)) 174 175 for k, v := range vars { 176 val, err := v.Decode(cty.DynamicPseudoType) 177 if err != nil { 178 return err 179 } 180 valJSON, err := ctyjson.Marshal(val, val.Type()) 181 if err != nil { 182 return err 183 } 184 p.Variables[k] = &variable{ 185 Value: valJSON, 186 } 187 } 188 return nil 189} 190 191func (p *plan) marshalResourceDrift(oldState, newState *states.State, schemas *terraform.Schemas) error { 192 // Our goal here is to build a data structure of the same shape as we use 193 // to describe planned resource changes, but in this case we'll be 194 // taking the old and new values from different state snapshots rather 195 // than from a real "Changes" object. 196 // 197 // In doing this we make an assumption that drift detection can only 198 // ever show objects as updated or removed, and will never show anything 199 // as created because we only refresh objects we were already tracking 200 // after the previous run. This means we can use oldState as our baseline 201 // for what resource instances we might include, and check for each item 202 // whether it's present in newState. If we ever have some mechanism to 203 // detect "additive drift" later then we'll need to take a different 204 // approach here, but we have no plans for that at the time of writing. 205 // 206 // We also assume that both states have had all managed resource objects 207 // upgraded to match the current schemas given in schemas, so we shouldn't 208 // need to contend with oldState having old-shaped objects even if the 209 // user changed provider versions since the last run. 210 211 if newState.ManagedResourcesEqual(oldState) { 212 // Nothing to do, because we only detect and report drift for managed 213 // resource instances. 214 return nil 215 } 216 for _, ms := range oldState.Modules { 217 for _, rs := range ms.Resources { 218 if rs.Addr.Resource.Mode != addrs.ManagedResourceMode { 219 // Drift reporting is only for managed resources 220 continue 221 } 222 223 provider := rs.ProviderConfig.Provider 224 for key, oldIS := range rs.Instances { 225 if oldIS.Current == nil { 226 // Not interested in instances that only have deposed objects 227 continue 228 } 229 addr := rs.Addr.Instance(key) 230 newIS := newState.ResourceInstance(addr) 231 232 schema, _ := schemas.ResourceTypeConfig( 233 provider, 234 addr.Resource.Resource.Mode, 235 addr.Resource.Resource.Type, 236 ) 237 if schema == nil { 238 return fmt.Errorf("no schema found for %s (in provider %s)", addr, provider) 239 } 240 ty := schema.ImpliedType() 241 242 oldObj, err := oldIS.Current.Decode(ty) 243 if err != nil { 244 return fmt.Errorf("failed to decode previous run data for %s: %s", addr, err) 245 } 246 247 var newObj *states.ResourceInstanceObject 248 if newIS != nil && newIS.Current != nil { 249 newObj, err = newIS.Current.Decode(ty) 250 if err != nil { 251 return fmt.Errorf("failed to decode refreshed data for %s: %s", addr, err) 252 } 253 } 254 255 var oldVal, newVal cty.Value 256 oldVal = oldObj.Value 257 if newObj != nil { 258 newVal = newObj.Value 259 } else { 260 newVal = cty.NullVal(ty) 261 } 262 263 if oldVal.RawEquals(newVal) { 264 // No drift if the two values are semantically equivalent 265 continue 266 } 267 268 oldSensitive := jsonstate.SensitiveAsBool(oldVal) 269 newSensitive := jsonstate.SensitiveAsBool(newVal) 270 oldVal, _ = oldVal.UnmarkDeep() 271 newVal, _ = newVal.UnmarkDeep() 272 273 var before, after []byte 274 var beforeSensitive, afterSensitive []byte 275 before, err = ctyjson.Marshal(oldVal, oldVal.Type()) 276 if err != nil { 277 return fmt.Errorf("failed to encode previous run data for %s as JSON: %s", addr, err) 278 } 279 after, err = ctyjson.Marshal(newVal, oldVal.Type()) 280 if err != nil { 281 return fmt.Errorf("failed to encode refreshed data for %s as JSON: %s", addr, err) 282 } 283 beforeSensitive, err = ctyjson.Marshal(oldSensitive, oldSensitive.Type()) 284 if err != nil { 285 return fmt.Errorf("failed to encode previous run data sensitivity for %s as JSON: %s", addr, err) 286 } 287 afterSensitive, err = ctyjson.Marshal(newSensitive, newSensitive.Type()) 288 if err != nil { 289 return fmt.Errorf("failed to encode refreshed data sensitivity for %s as JSON: %s", addr, err) 290 } 291 292 // We can only detect updates and deletes as drift. 293 action := plans.Update 294 if newVal.IsNull() { 295 action = plans.Delete 296 } 297 298 change := resourceChange{ 299 Address: addr.String(), 300 ModuleAddress: addr.Module.String(), 301 Mode: "managed", // drift reporting is only for managed resources 302 Name: addr.Resource.Resource.Name, 303 Type: addr.Resource.Resource.Type, 304 ProviderName: provider.String(), 305 306 Change: change{ 307 Actions: actionString(action.String()), 308 Before: json.RawMessage(before), 309 BeforeSensitive: json.RawMessage(beforeSensitive), 310 After: json.RawMessage(after), 311 AfterSensitive: json.RawMessage(afterSensitive), 312 // AfterUnknown is never populated here because 313 // values in a state are always fully known. 314 }, 315 } 316 p.ResourceDrift = append(p.ResourceDrift, change) 317 } 318 } 319 } 320 321 sort.Slice(p.ResourceChanges, func(i, j int) bool { 322 return p.ResourceChanges[i].Address < p.ResourceChanges[j].Address 323 }) 324 325 return nil 326} 327 328func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform.Schemas) error { 329 if changes == nil { 330 // Nothing to do! 331 return nil 332 } 333 for _, rc := range changes.Resources { 334 var r resourceChange 335 addr := rc.Addr 336 r.Address = addr.String() 337 338 dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode 339 // We create "delete" actions for data resources so we can clean up 340 // their entries in state, but this is an implementation detail that 341 // users shouldn't see. 342 if dataSource && rc.Action == plans.Delete { 343 continue 344 } 345 346 schema, _ := schemas.ResourceTypeConfig( 347 rc.ProviderAddr.Provider, 348 addr.Resource.Resource.Mode, 349 addr.Resource.Resource.Type, 350 ) 351 if schema == nil { 352 return fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) 353 } 354 355 changeV, err := rc.Decode(schema.ImpliedType()) 356 if err != nil { 357 return err 358 } 359 // We drop the marks from the change, as decoding is only an 360 // intermediate step to re-encode the values as json 361 changeV.Before, _ = changeV.Before.UnmarkDeep() 362 changeV.After, _ = changeV.After.UnmarkDeep() 363 364 var before, after []byte 365 var beforeSensitive, afterSensitive []byte 366 var afterUnknown cty.Value 367 368 if changeV.Before != cty.NilVal { 369 before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) 370 if err != nil { 371 return err 372 } 373 marks := rc.BeforeValMarks 374 if schema.ContainsSensitive() { 375 marks = append(marks, schema.ValueMarks(changeV.Before, nil)...) 376 } 377 bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks)) 378 beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) 379 if err != nil { 380 return err 381 } 382 } 383 if changeV.After != cty.NilVal { 384 if changeV.After.IsWhollyKnown() { 385 after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) 386 if err != nil { 387 return err 388 } 389 afterUnknown = cty.EmptyObjectVal 390 } else { 391 filteredAfter := omitUnknowns(changeV.After) 392 if filteredAfter.IsNull() { 393 after = nil 394 } else { 395 after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) 396 if err != nil { 397 return err 398 } 399 } 400 afterUnknown = unknownAsBool(changeV.After) 401 } 402 marks := rc.AfterValMarks 403 if schema.ContainsSensitive() { 404 marks = append(marks, schema.ValueMarks(changeV.After, nil)...) 405 } 406 as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks)) 407 afterSensitive, err = ctyjson.Marshal(as, as.Type()) 408 if err != nil { 409 return err 410 } 411 } 412 413 a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) 414 if err != nil { 415 return err 416 } 417 replacePaths, err := encodePaths(rc.RequiredReplace) 418 if err != nil { 419 return err 420 } 421 422 r.Change = change{ 423 Actions: actionString(rc.Action.String()), 424 Before: json.RawMessage(before), 425 After: json.RawMessage(after), 426 AfterUnknown: a, 427 BeforeSensitive: json.RawMessage(beforeSensitive), 428 AfterSensitive: json.RawMessage(afterSensitive), 429 ReplacePaths: replacePaths, 430 } 431 432 if rc.DeposedKey != states.NotDeposed { 433 r.Deposed = rc.DeposedKey.String() 434 } 435 436 key := addr.Resource.Key 437 if key != nil { 438 r.Index = key 439 } 440 441 switch addr.Resource.Resource.Mode { 442 case addrs.ManagedResourceMode: 443 r.Mode = "managed" 444 case addrs.DataResourceMode: 445 r.Mode = "data" 446 default: 447 return fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String()) 448 } 449 r.ModuleAddress = addr.Module.String() 450 r.Name = addr.Resource.Resource.Name 451 r.Type = addr.Resource.Resource.Type 452 r.ProviderName = rc.ProviderAddr.Provider.String() 453 454 switch rc.ActionReason { 455 case plans.ResourceInstanceChangeNoReason: 456 r.ActionReason = "" // will be omitted in output 457 case plans.ResourceInstanceReplaceBecauseCannotUpdate: 458 r.ActionReason = "replace_because_cannot_update" 459 case plans.ResourceInstanceReplaceBecauseTainted: 460 r.ActionReason = "replace_because_tainted" 461 case plans.ResourceInstanceReplaceByRequest: 462 r.ActionReason = "replace_by_request" 463 default: 464 return fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) 465 } 466 467 p.ResourceChanges = append(p.ResourceChanges, r) 468 469 } 470 471 sort.Slice(p.ResourceChanges, func(i, j int) bool { 472 return p.ResourceChanges[i].Address < p.ResourceChanges[j].Address 473 }) 474 475 return nil 476} 477 478func (p *plan) marshalOutputChanges(changes *plans.Changes) error { 479 if changes == nil { 480 // Nothing to do! 481 return nil 482 } 483 484 p.OutputChanges = make(map[string]change, len(changes.Outputs)) 485 for _, oc := range changes.Outputs { 486 changeV, err := oc.Decode() 487 if err != nil { 488 return err 489 } 490 // We drop the marks from the change, as decoding is only an 491 // intermediate step to re-encode the values as json 492 changeV.Before, _ = changeV.Before.UnmarkDeep() 493 changeV.After, _ = changeV.After.UnmarkDeep() 494 495 var before, after []byte 496 afterUnknown := cty.False 497 if changeV.Before != cty.NilVal { 498 before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) 499 if err != nil { 500 return err 501 } 502 } 503 if changeV.After != cty.NilVal { 504 if changeV.After.IsWhollyKnown() { 505 after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) 506 if err != nil { 507 return err 508 } 509 } else { 510 afterUnknown = cty.True 511 } 512 } 513 514 // The only information we have in the plan about output sensitivity is 515 // a boolean which is true if the output was or is marked sensitive. As 516 // a result, BeforeSensitive and AfterSensitive will be identical, and 517 // either false or true. 518 outputSensitive := cty.False 519 if oc.Sensitive { 520 outputSensitive = cty.True 521 } 522 sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type()) 523 if err != nil { 524 return err 525 } 526 527 a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) 528 529 c := change{ 530 Actions: actionString(oc.Action.String()), 531 Before: json.RawMessage(before), 532 After: json.RawMessage(after), 533 AfterUnknown: a, 534 BeforeSensitive: json.RawMessage(sensitive), 535 AfterSensitive: json.RawMessage(sensitive), 536 } 537 538 p.OutputChanges[oc.Addr.OutputValue.Name] = c 539 } 540 541 return nil 542} 543 544func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error { 545 // marshal the planned changes into a module 546 plan, err := marshalPlannedValues(changes, schemas) 547 if err != nil { 548 return err 549 } 550 p.PlannedValues.RootModule = plan 551 552 // marshalPlannedOutputs 553 outputs, err := marshalPlannedOutputs(changes) 554 if err != nil { 555 return err 556 } 557 p.PlannedValues.Outputs = outputs 558 559 return nil 560} 561 562// omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, 563// omitting any unknowns. 564// 565// The result also normalizes some types: all sequence types are turned into 566// tuple types and all mapping types are converted to object types, since we 567// assume the result of this is just going to be serialized as JSON (and thus 568// lose those distinctions) anyway. 569func omitUnknowns(val cty.Value) cty.Value { 570 ty := val.Type() 571 switch { 572 case val.IsNull(): 573 return val 574 case !val.IsKnown(): 575 return cty.NilVal 576 case ty.IsPrimitiveType(): 577 return val 578 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 579 var vals []cty.Value 580 it := val.ElementIterator() 581 for it.Next() { 582 _, v := it.Element() 583 newVal := omitUnknowns(v) 584 if newVal != cty.NilVal { 585 vals = append(vals, newVal) 586 } else if newVal == cty.NilVal && ty.IsListType() { 587 // list length may be significant, so we will turn unknowns into nulls 588 vals = append(vals, cty.NullVal(v.Type())) 589 } 590 } 591 // We use tuple types always here, because the work we did above 592 // may have caused the individual elements to have different types, 593 // and we're doing this work to produce JSON anyway and JSON marshalling 594 // represents all of these sequence types as an array. 595 return cty.TupleVal(vals) 596 case ty.IsMapType() || ty.IsObjectType(): 597 vals := make(map[string]cty.Value) 598 it := val.ElementIterator() 599 for it.Next() { 600 k, v := it.Element() 601 newVal := omitUnknowns(v) 602 if newVal != cty.NilVal { 603 vals[k.AsString()] = newVal 604 } 605 } 606 // We use object types always here, because the work we did above 607 // may have caused the individual elements to have different types, 608 // and we're doing this work to produce JSON anyway and JSON marshalling 609 // represents both of these mapping types as an object. 610 return cty.ObjectVal(vals) 611 default: 612 // Should never happen, since the above should cover all types 613 panic(fmt.Sprintf("omitUnknowns cannot handle %#v", val)) 614 } 615} 616 617// recursively iterate through a cty.Value, replacing unknown values (including 618// null) with cty.True and known values with cty.False. 619// 620// The result also normalizes some types: all sequence types are turned into 621// tuple types and all mapping types are converted to object types, since we 622// assume the result of this is just going to be serialized as JSON (and thus 623// lose those distinctions) anyway. 624// 625// For map/object values, all known attribute values will be omitted instead of 626// returning false, as this results in a more compact serialization. 627func unknownAsBool(val cty.Value) cty.Value { 628 ty := val.Type() 629 switch { 630 case val.IsNull(): 631 return cty.False 632 case !val.IsKnown(): 633 if ty.IsPrimitiveType() || ty.Equals(cty.DynamicPseudoType) { 634 return cty.True 635 } 636 fallthrough 637 case ty.IsPrimitiveType(): 638 return cty.BoolVal(!val.IsKnown()) 639 case ty.IsListType() || ty.IsTupleType() || ty.IsSetType(): 640 length := val.LengthInt() 641 if length == 0 { 642 // If there are no elements then we can't have unknowns 643 return cty.EmptyTupleVal 644 } 645 vals := make([]cty.Value, 0, length) 646 it := val.ElementIterator() 647 for it.Next() { 648 _, v := it.Element() 649 vals = append(vals, unknownAsBool(v)) 650 } 651 // The above transform may have changed the types of some of the 652 // elements, so we'll always use a tuple here in case we've now made 653 // different elements have different types. Our ultimate goal is to 654 // marshal to JSON anyway, and all of these sequence types are 655 // indistinguishable in JSON. 656 return cty.TupleVal(vals) 657 case ty.IsMapType() || ty.IsObjectType(): 658 var length int 659 switch { 660 case ty.IsMapType(): 661 length = val.LengthInt() 662 default: 663 length = len(val.Type().AttributeTypes()) 664 } 665 if length == 0 { 666 // If there are no elements then we can't have unknowns 667 return cty.EmptyObjectVal 668 } 669 vals := make(map[string]cty.Value) 670 it := val.ElementIterator() 671 for it.Next() { 672 k, v := it.Element() 673 vAsBool := unknownAsBool(v) 674 // Omit all of the "false"s for known values for more compact 675 // serialization 676 if !vAsBool.RawEquals(cty.False) { 677 vals[k.AsString()] = unknownAsBool(v) 678 } 679 } 680 // The above transform may have changed the types of some of the 681 // elements, so we'll always use an object here in case we've now made 682 // different elements have different types. Our ultimate goal is to 683 // marshal to JSON anyway, and all of these mapping types are 684 // indistinguishable in JSON. 685 return cty.ObjectVal(vals) 686 default: 687 // Should never happen, since the above should cover all types 688 panic(fmt.Sprintf("unknownAsBool cannot handle %#v", val)) 689 } 690} 691 692func actionString(action string) []string { 693 switch { 694 case action == "NoOp": 695 return []string{"no-op"} 696 case action == "Create": 697 return []string{"create"} 698 case action == "Delete": 699 return []string{"delete"} 700 case action == "Update": 701 return []string{"update"} 702 case action == "CreateThenDelete": 703 return []string{"create", "delete"} 704 case action == "Read": 705 return []string{"read"} 706 case action == "DeleteThenCreate": 707 return []string{"delete", "create"} 708 default: 709 return []string{action} 710 } 711} 712 713// encodePaths lossily encodes a cty.PathSet into an array of arrays of step 714// values, such as: 715// 716// [["length"],["triggers",0,"value"]] 717// 718// The lossiness is that we cannot distinguish between an IndexStep with string 719// key and a GetAttr step. This is fine with JSON output, because JSON's type 720// system means that those two steps are equivalent anyway: both are object 721// indexes. 722// 723// JavaScript (or similar dynamic language) consumers of these values can 724// recursively apply the steps to a given object using an index operation for 725// each step. 726func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { 727 if pathSet.Empty() { 728 return nil, nil 729 } 730 731 pathList := pathSet.List() 732 jsonPaths := make([]json.RawMessage, 0, len(pathList)) 733 734 for _, path := range pathList { 735 steps := make([]json.RawMessage, 0, len(path)) 736 for _, step := range path { 737 switch s := step.(type) { 738 case cty.IndexStep: 739 key, err := ctyjson.Marshal(s.Key, s.Key.Type()) 740 if err != nil { 741 return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err) 742 } 743 steps = append(steps, key) 744 case cty.GetAttrStep: 745 name, err := json.Marshal(s.Name) 746 if err != nil { 747 return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err) 748 } 749 steps = append(steps, name) 750 default: 751 return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) 752 } 753 } 754 jsonPath, err := json.Marshal(steps) 755 if err != nil { 756 return nil, err 757 } 758 jsonPaths = append(jsonPaths, jsonPath) 759 } 760 761 return json.Marshal(jsonPaths) 762} 763