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/jsonstate"
13	"github.com/hashicorp/terraform/internal/configs/configschema"
14	"github.com/hashicorp/terraform/internal/plans"
15	"github.com/hashicorp/terraform/internal/states"
16	"github.com/hashicorp/terraform/internal/terraform"
17)
18
19// stateValues is the common representation of resolved values for both the
20// prior state (which is always complete) and the planned new state.
21type stateValues struct {
22	Outputs    map[string]output `json:"outputs,omitempty"`
23	RootModule module            `json:"root_module,omitempty"`
24}
25
26// attributeValues is the JSON representation of the attribute values of the
27// resource, whose structure depends on the resource type schema.
28type attributeValues map[string]interface{}
29
30func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
31	if value == cty.NilVal || value.IsNull() {
32		return nil
33	}
34	ret := make(attributeValues)
35
36	it := value.ElementIterator()
37	for it.Next() {
38		k, v := it.Element()
39		vJSON, _ := ctyjson.Marshal(v, v.Type())
40		ret[k.AsString()] = json.RawMessage(vJSON)
41	}
42	return ret
43}
44
45// marshalPlannedOutputs takes a list of changes and returns a map of output
46// values
47func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
48	if changes.Outputs == nil {
49		// No changes - we're done here!
50		return nil, nil
51	}
52
53	ret := make(map[string]output)
54
55	for _, oc := range changes.Outputs {
56		if oc.ChangeSrc.Action == plans.Delete {
57			continue
58		}
59
60		var after []byte
61		changeV, err := oc.Decode()
62		if err != nil {
63			return ret, err
64		}
65		// The values may be marked, but we must rely on the Sensitive flag
66		// as the decoded value is only an intermediate step in transcoding
67		// this to a json format.
68		changeV.After, _ = changeV.After.UnmarkDeep()
69
70		if changeV.After != cty.NilVal && changeV.After.IsWhollyKnown() {
71			after, err = ctyjson.Marshal(changeV.After, changeV.After.Type())
72			if err != nil {
73				return ret, err
74			}
75		}
76
77		ret[oc.Addr.OutputValue.Name] = output{
78			Value:     json.RawMessage(after),
79			Sensitive: oc.Sensitive,
80		}
81	}
82
83	return ret, nil
84
85}
86
87func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) {
88	var ret module
89
90	// build two maps:
91	// 		module name -> [resource addresses]
92	// 		module -> [children modules]
93	moduleResourceMap := make(map[string][]addrs.AbsResourceInstance)
94	moduleMap := make(map[string][]addrs.ModuleInstance)
95	seenModules := make(map[string]bool)
96
97	for _, resource := range changes.Resources {
98		// If the resource is being deleted, skip over it.
99		// Deposed instances are always conceptually a destroy, but if they
100		// were gone during refresh then the change becomes a noop.
101		if resource.Action != plans.Delete && resource.DeposedKey == states.NotDeposed {
102			containingModule := resource.Addr.Module.String()
103			moduleResourceMap[containingModule] = append(moduleResourceMap[containingModule], resource.Addr)
104
105			// the root module has no parents
106			if !resource.Addr.Module.IsRoot() {
107				parent := resource.Addr.Module.Parent().String()
108				// we expect to see multiple resources in one module, so we
109				// only need to report the "parent" module for each child module
110				// once.
111				if !seenModules[containingModule] {
112					moduleMap[parent] = append(moduleMap[parent], resource.Addr.Module)
113					seenModules[containingModule] = true
114				}
115
116				// If any given parent module has no resources, it needs to be
117				// added to the moduleMap. This walks through the current
118				// resources' modules' ancestors, taking advantage of the fact
119				// that Ancestors() returns an ordered slice, and verifies that
120				// each one is in the map.
121				ancestors := resource.Addr.Module.Ancestors()
122				for i, ancestor := range ancestors[:len(ancestors)-1] {
123					aStr := ancestor.String()
124
125					// childStr here is the immediate child of the current step
126					childStr := ancestors[i+1].String()
127					// we likely will see multiple resources in one module, so we
128					// only need to report the "parent" module for each child module
129					// once.
130					if !seenModules[childStr] {
131						moduleMap[aStr] = append(moduleMap[aStr], ancestors[i+1])
132						seenModules[childStr] = true
133					}
134				}
135			}
136		}
137	}
138
139	// start with the root module
140	resources, err := marshalPlanResources(changes, moduleResourceMap[""], schemas)
141	if err != nil {
142		return ret, err
143	}
144	ret.Resources = resources
145
146	childModules, err := marshalPlanModules(changes, schemas, moduleMap[""], moduleMap, moduleResourceMap)
147	if err != nil {
148		return ret, err
149	}
150	sort.Slice(childModules, func(i, j int) bool {
151		return childModules[i].Address < childModules[j].Address
152	})
153
154	ret.ChildModules = childModules
155
156	return ret, nil
157}
158
159// marshalPlanResources
160func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) {
161	var ret []resource
162
163	for _, ri := range ris {
164		r := changes.ResourceInstance(ri)
165		if r.Action == plans.Delete {
166			continue
167		}
168
169		resource := resource{
170			Address:      r.Addr.String(),
171			Type:         r.Addr.Resource.Resource.Type,
172			Name:         r.Addr.Resource.Resource.Name,
173			ProviderName: r.ProviderAddr.Provider.String(),
174			Index:        r.Addr.Resource.Key,
175		}
176
177		switch r.Addr.Resource.Resource.Mode {
178		case addrs.ManagedResourceMode:
179			resource.Mode = "managed"
180		case addrs.DataResourceMode:
181			resource.Mode = "data"
182		default:
183			return nil, fmt.Errorf("resource %s has an unsupported mode %s",
184				r.Addr.String(),
185				r.Addr.Resource.Resource.Mode.String(),
186			)
187		}
188
189		schema, schemaVer := schemas.ResourceTypeConfig(
190			r.ProviderAddr.Provider,
191			r.Addr.Resource.Resource.Mode,
192			resource.Type,
193		)
194		if schema == nil {
195			return nil, fmt.Errorf("no schema found for %s", r.Addr.String())
196		}
197		resource.SchemaVersion = schemaVer
198		changeV, err := r.Decode(schema.ImpliedType())
199		if err != nil {
200			return nil, err
201		}
202
203		// copy the marked After values so we can use these in marshalSensitiveValues
204		markedAfter := changeV.After
205
206		// The values may be marked, but we must rely on the Sensitive flag
207		// as the decoded value is only an intermediate step in transcoding
208		// this to a json format.
209		changeV.Before, _ = changeV.Before.UnmarkDeep()
210		changeV.After, _ = changeV.After.UnmarkDeep()
211
212		if changeV.After != cty.NilVal {
213			if changeV.After.IsWhollyKnown() {
214				resource.AttributeValues = marshalAttributeValues(changeV.After, schema)
215			} else {
216				knowns := omitUnknowns(changeV.After)
217				resource.AttributeValues = marshalAttributeValues(knowns, schema)
218			}
219		}
220
221		s := jsonstate.SensitiveAsBool(markedAfter)
222		v, err := ctyjson.Marshal(s, s.Type())
223		if err != nil {
224			return nil, err
225		}
226		resource.SensitiveValues = v
227
228		ret = append(ret, resource)
229	}
230
231	sort.Slice(ret, func(i, j int) bool {
232		return ret[i].Address < ret[j].Address
233	})
234
235	return ret, nil
236}
237
238// marshalPlanModules iterates over a list of modules to recursively describe
239// the full module tree.
240func marshalPlanModules(
241	changes *plans.Changes,
242	schemas *terraform.Schemas,
243	childModules []addrs.ModuleInstance,
244	moduleMap map[string][]addrs.ModuleInstance,
245	moduleResourceMap map[string][]addrs.AbsResourceInstance,
246) ([]module, error) {
247
248	var ret []module
249
250	for _, child := range childModules {
251		moduleResources := moduleResourceMap[child.String()]
252		// cm for child module, naming things is hard.
253		var cm module
254		// don't populate the address for the root module
255		if child.String() != "" {
256			cm.Address = child.String()
257		}
258		rs, err := marshalPlanResources(changes, moduleResources, schemas)
259		if err != nil {
260			return nil, err
261		}
262		cm.Resources = rs
263
264		if len(moduleMap[child.String()]) > 0 {
265			moreChildModules, err := marshalPlanModules(changes, schemas, moduleMap[child.String()], moduleMap, moduleResourceMap)
266			if err != nil {
267				return nil, err
268			}
269			cm.ChildModules = moreChildModules
270		}
271
272		ret = append(ret, cm)
273	}
274
275	return ret, nil
276}
277
278// marshalSensitiveValues returns a map of sensitive attributes, with the value
279// set to true. It returns nil if the value is nil or if there are no sensitive
280// vals.
281func marshalSensitiveValues(value cty.Value) map[string]bool {
282	if value.RawEquals(cty.NilVal) || value.IsNull() {
283		return nil
284	}
285
286	ret := make(map[string]bool)
287
288	it := value.ElementIterator()
289	for it.Next() {
290		k, v := it.Element()
291		s := jsonstate.SensitiveAsBool(v)
292		if !s.RawEquals(cty.False) {
293			ret[k.AsString()] = true
294		}
295	}
296
297	if len(ret) == 0 {
298		return nil
299	}
300	return ret
301}
302