1package hcl2shim
2
3import (
4	"fmt"
5	"strconv"
6	"strings"
7
8	"github.com/zclconf/go-cty/cty/convert"
9
10	"github.com/zclconf/go-cty/cty"
11)
12
13// FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
14// types library that HCL2 uses) to a map compatible with what would be
15// produced by the "flatmap" package.
16//
17// The type of the given value informs the structure of the resulting map.
18// The value must be of an object type or this function will panic.
19//
20// Flatmap values can only represent maps when they are of primitive types,
21// so the given value must not have any maps of complex types or the result
22// is undefined.
23func FlatmapValueFromHCL2(v cty.Value) map[string]string {
24	if v.IsNull() {
25		return nil
26	}
27
28	if !v.Type().IsObjectType() {
29		panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type()))
30	}
31
32	m := make(map[string]string)
33	flatmapValueFromHCL2Map(m, "", v)
34	return m
35}
36
37func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) {
38	ty := val.Type()
39	switch {
40	case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType:
41		flatmapValueFromHCL2Primitive(m, key, val)
42	case ty.IsObjectType() || ty.IsMapType():
43		flatmapValueFromHCL2Map(m, key+".", val)
44	case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
45		flatmapValueFromHCL2Seq(m, key+".", val)
46	default:
47		panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName()))
48	}
49}
50
51func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) {
52	if !val.IsKnown() {
53		m[key] = UnknownVariableValue
54		return
55	}
56	if val.IsNull() {
57		// Omit entirely
58		return
59	}
60
61	var err error
62	val, err = convert.Convert(val, cty.String)
63	if err != nil {
64		// Should not be possible, since all primitive types can convert to string.
65		panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err))
66	}
67	m[key] = val.AsString()
68}
69
70func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) {
71	if val.IsNull() {
72		// Omit entirely
73		return
74	}
75	if !val.IsKnown() {
76		switch {
77		case val.Type().IsObjectType():
78			// Whole objects can't be unknown in flatmap, so instead we'll
79			// just write all of the attribute values out as unknown.
80			for name, aty := range val.Type().AttributeTypes() {
81				flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty))
82			}
83		default:
84			m[prefix+"%"] = UnknownVariableValue
85		}
86		return
87	}
88
89	len := 0
90	for it := val.ElementIterator(); it.Next(); {
91		ak, av := it.Element()
92		name := ak.AsString()
93		flatmapValueFromHCL2Value(m, prefix+name, av)
94		len++
95	}
96	if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed
97		m[prefix+"%"] = strconv.Itoa(len)
98	}
99}
100
101func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) {
102	if val.IsNull() {
103		// Omit entirely
104		return
105	}
106	if !val.IsKnown() {
107		m[prefix+"#"] = UnknownVariableValue
108		return
109	}
110
111	// For sets this won't actually generate exactly what helper/schema would've
112	// generated, because we don't have access to the set key function it
113	// would've used. However, in practice it doesn't actually matter what the
114	// keys are as long as they are unique, so we'll just generate sequential
115	// indexes for them as if it were a list.
116	//
117	// An important implication of this, however, is that the set ordering will
118	// not be consistent across mutations and so different keys may be assigned
119	// to the same value when round-tripping. Since this shim is intended to
120	// be short-lived and not used for round-tripping, we accept this.
121	i := 0
122	for it := val.ElementIterator(); it.Next(); {
123		_, av := it.Element()
124		key := prefix + strconv.Itoa(i)
125		flatmapValueFromHCL2Value(m, key, av)
126		i++
127	}
128	m[prefix+"#"] = strconv.Itoa(i)
129}
130
131// HCL2ValueFromFlatmap converts a map compatible with what would be produced
132// by the "flatmap" package to a HCL2 (really, the cty dynamic types library
133// that HCL2 uses) object type.
134//
135// The intended result type must be provided in order to guide how the
136// map contents are decoded. This must be an object type or this function
137// will panic.
138//
139// Flatmap values can only represent maps when they are of primitive types,
140// so the given type must not have any maps of complex types or the result
141// is undefined.
142//
143// The result may contain null values if the given map does not contain keys
144// for all of the different key paths implied by the given type.
145func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) {
146	if m == nil {
147		return cty.NullVal(ty), nil
148	}
149	if !ty.IsObjectType() {
150		panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty))
151	}
152
153	return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes())
154}
155
156func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
157	var val cty.Value
158	var err error
159	switch {
160	case ty.IsPrimitiveType():
161		val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty)
162	case ty.IsObjectType():
163		val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes())
164	case ty.IsTupleType():
165		val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes())
166	case ty.IsMapType():
167		val, err = hcl2ValueFromFlatmapMap(m, key+".", ty)
168	case ty.IsListType():
169		val, err = hcl2ValueFromFlatmapList(m, key+".", ty)
170	case ty.IsSetType():
171		val, err = hcl2ValueFromFlatmapSet(m, key+".", ty)
172	default:
173		err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName())
174	}
175
176	if err != nil {
177		return cty.DynamicVal, err
178	}
179	return val, nil
180}
181
182func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
183	rawVal, exists := m[key]
184	if !exists {
185		return cty.NullVal(ty), nil
186	}
187	if rawVal == UnknownVariableValue {
188		return cty.UnknownVal(ty), nil
189	}
190
191	var err error
192	val := cty.StringVal(rawVal)
193	val, err = convert.Convert(val, ty)
194	if err != nil {
195		// This should never happen for _valid_ input, but flatmap data might
196		// be tampered with by the user and become invalid.
197		return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %s", key, err)
198	}
199
200	return val, nil
201}
202
203func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) {
204	vals := make(map[string]cty.Value)
205	for name, aty := range atys {
206		val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty)
207		if err != nil {
208			return cty.DynamicVal, err
209		}
210		vals[name] = val
211	}
212	return cty.ObjectVal(vals), nil
213}
214
215func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) {
216	var vals []cty.Value
217
218	// if the container is unknown, there is no count string
219	listName := strings.TrimRight(prefix, ".")
220	if m[listName] == UnknownVariableValue {
221		return cty.UnknownVal(cty.Tuple(etys)), nil
222	}
223
224	countStr, exists := m[prefix+"#"]
225	if !exists {
226		return cty.NullVal(cty.Tuple(etys)), nil
227	}
228	if countStr == UnknownVariableValue {
229		return cty.UnknownVal(cty.Tuple(etys)), nil
230	}
231
232	count, err := strconv.Atoi(countStr)
233	if err != nil {
234		return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
235	}
236	if count != len(etys) {
237		return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys))
238	}
239
240	vals = make([]cty.Value, len(etys))
241	for i, ety := range etys {
242		key := prefix + strconv.Itoa(i)
243		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
244		if err != nil {
245			return cty.DynamicVal, err
246		}
247		vals[i] = val
248	}
249	return cty.TupleVal(vals), nil
250}
251
252func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
253	vals := make(map[string]cty.Value)
254	ety := ty.ElementType()
255
256	// if the container is unknown, there is no count string
257	listName := strings.TrimRight(prefix, ".")
258	if m[listName] == UnknownVariableValue {
259		return cty.UnknownVal(ty), nil
260	}
261
262	// We actually don't really care about the "count" of a map for our
263	// purposes here, but we do need to check if it _exists_ in order to
264	// recognize the difference between null (not set at all) and empty.
265	if strCount, exists := m[prefix+"%"]; !exists {
266		return cty.NullVal(ty), nil
267	} else if strCount == UnknownVariableValue {
268		return cty.UnknownVal(ty), nil
269	}
270
271	for fullKey := range m {
272		if !strings.HasPrefix(fullKey, prefix) {
273			continue
274		}
275
276		// The flatmap format doesn't allow us to distinguish between keys
277		// that contain periods and nested objects, so by convention a
278		// map is only ever of primitive type in flatmap, and we just assume
279		// that the remainder of the raw key (dots and all) is the key we
280		// want in the result value.
281		key := fullKey[len(prefix):]
282		if key == "%" {
283			// Ignore the "count" key
284			continue
285		}
286
287		val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety)
288		if err != nil {
289			return cty.DynamicVal, err
290		}
291		vals[key] = val
292	}
293
294	if len(vals) == 0 {
295		return cty.MapValEmpty(ety), nil
296	}
297	return cty.MapVal(vals), nil
298}
299
300func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
301	var vals []cty.Value
302
303	// if the container is unknown, there is no count string
304	listName := strings.TrimRight(prefix, ".")
305	if m[listName] == UnknownVariableValue {
306		return cty.UnknownVal(ty), nil
307	}
308
309	countStr, exists := m[prefix+"#"]
310	if !exists {
311		return cty.NullVal(ty), nil
312	}
313	if countStr == UnknownVariableValue {
314		return cty.UnknownVal(ty), nil
315	}
316
317	count, err := strconv.Atoi(countStr)
318	if err != nil {
319		return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
320	}
321
322	ety := ty.ElementType()
323	if count == 0 {
324		return cty.ListValEmpty(ety), nil
325	}
326
327	vals = make([]cty.Value, count)
328	for i := 0; i < count; i++ {
329		key := prefix + strconv.Itoa(i)
330		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
331		if err != nil {
332			return cty.DynamicVal, err
333		}
334		vals[i] = val
335	}
336
337	return cty.ListVal(vals), nil
338}
339
340func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
341	var vals []cty.Value
342	ety := ty.ElementType()
343
344	// if the container is unknown, there is no count string
345	listName := strings.TrimRight(prefix, ".")
346	if m[listName] == UnknownVariableValue {
347		return cty.UnknownVal(ty), nil
348	}
349
350	strCount, exists := m[prefix+"#"]
351	if !exists {
352		return cty.NullVal(ty), nil
353	} else if strCount == UnknownVariableValue {
354		return cty.UnknownVal(ty), nil
355	}
356
357	// Keep track of keys we've seen, se we don't add the same set value
358	// multiple times. The cty.Set will normally de-duplicate values, but we may
359	// have unknown values that would not show as equivalent.
360	seen := map[string]bool{}
361
362	for fullKey := range m {
363		if !strings.HasPrefix(fullKey, prefix) {
364			continue
365		}
366		subKey := fullKey[len(prefix):]
367		if subKey == "#" {
368			// Ignore the "count" key
369			continue
370		}
371		key := fullKey
372		if dot := strings.IndexByte(subKey, '.'); dot != -1 {
373			key = fullKey[:dot+len(prefix)]
374		}
375
376		if seen[key] {
377			continue
378		}
379
380		seen[key] = true
381
382		// The flatmap format doesn't allow us to distinguish between keys
383		// that contain periods and nested objects, so by convention a
384		// map is only ever of primitive type in flatmap, and we just assume
385		// that the remainder of the raw key (dots and all) is the key we
386		// want in the result value.
387
388		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
389		if err != nil {
390			return cty.DynamicVal, err
391		}
392		vals = append(vals, val)
393	}
394
395	if len(vals) == 0 && strCount == "1" {
396		// An empty set wouldn't be represented in the flatmap, so this must be
397		// a single empty object since the count is actually 1.
398		// Add an appropriately typed null value to the set.
399		var val cty.Value
400		switch {
401		case ety.IsMapType():
402			val = cty.MapValEmpty(ety)
403		case ety.IsListType():
404			val = cty.ListValEmpty(ety)
405		case ety.IsSetType():
406			val = cty.SetValEmpty(ety)
407		case ety.IsObjectType():
408			// TODO: cty.ObjectValEmpty
409			objectMap := map[string]cty.Value{}
410			for attr, ty := range ety.AttributeTypes() {
411				objectMap[attr] = cty.NullVal(ty)
412			}
413			val = cty.ObjectVal(objectMap)
414		default:
415			val = cty.NullVal(ety)
416		}
417		vals = append(vals, val)
418
419	} else if len(vals) == 0 {
420		return cty.SetValEmpty(ety), nil
421	}
422
423	return cty.SetVal(vals), nil
424}
425