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