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