| package hcl2shim |
| |
| import ( |
| "fmt" |
| "strconv" |
| "strings" |
| |
| "github.com/zclconf/go-cty/cty/convert" |
| |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic |
| // types library that HCL2 uses) to a map compatible with what would be |
| // produced by the "flatmap" package. |
| // |
| // The type of the given value informs the structure of the resulting map. |
| // The value must be of an object type or this function will panic. |
| // |
| // Flatmap values can only represent maps when they are of primitive types, |
| // so the given value must not have any maps of complex types or the result |
| // is undefined. |
| func FlatmapValueFromHCL2(v cty.Value) map[string]string { |
| if v.IsNull() { |
| return nil |
| } |
| |
| if !v.Type().IsObjectType() { |
| panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type())) |
| } |
| |
| m := make(map[string]string) |
| flatmapValueFromHCL2Map(m, "", v) |
| return m |
| } |
| |
| func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) { |
| ty := val.Type() |
| switch { |
| case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType: |
| flatmapValueFromHCL2Primitive(m, key, val) |
| case ty.IsObjectType() || ty.IsMapType(): |
| flatmapValueFromHCL2Map(m, key+".", val) |
| case ty.IsTupleType() || ty.IsListType() || ty.IsSetType(): |
| flatmapValueFromHCL2Seq(m, key+".", val) |
| default: |
| panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName())) |
| } |
| } |
| |
| func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) { |
| if !val.IsKnown() { |
| m[key] = UnknownVariableValue |
| return |
| } |
| if val.IsNull() { |
| // Omit entirely |
| return |
| } |
| |
| var err error |
| val, err = convert.Convert(val, cty.String) |
| if err != nil { |
| // Should not be possible, since all primitive types can convert to string. |
| panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err)) |
| } |
| m[key] = val.AsString() |
| } |
| |
| func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) { |
| if val.IsNull() { |
| // Omit entirely |
| return |
| } |
| if !val.IsKnown() { |
| switch { |
| case val.Type().IsObjectType(): |
| // Whole objects can't be unknown in flatmap, so instead we'll |
| // just write all of the attribute values out as unknown. |
| for name, aty := range val.Type().AttributeTypes() { |
| flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty)) |
| } |
| default: |
| m[prefix+"%"] = UnknownVariableValue |
| } |
| return |
| } |
| |
| len := 0 |
| for it := val.ElementIterator(); it.Next(); { |
| ak, av := it.Element() |
| name := ak.AsString() |
| flatmapValueFromHCL2Value(m, prefix+name, av) |
| len++ |
| } |
| if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed |
| m[prefix+"%"] = strconv.Itoa(len) |
| } |
| } |
| |
| func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) { |
| if val.IsNull() { |
| // Omit entirely |
| return |
| } |
| if !val.IsKnown() { |
| m[prefix+"#"] = UnknownVariableValue |
| return |
| } |
| |
| // For sets this won't actually generate exactly what helper/schema would've |
| // generated, because we don't have access to the set key function it |
| // would've used. However, in practice it doesn't actually matter what the |
| // keys are as long as they are unique, so we'll just generate sequential |
| // indexes for them as if it were a list. |
| // |
| // An important implication of this, however, is that the set ordering will |
| // not be consistent across mutations and so different keys may be assigned |
| // to the same value when round-tripping. Since this shim is intended to |
| // be short-lived and not used for round-tripping, we accept this. |
| i := 0 |
| for it := val.ElementIterator(); it.Next(); { |
| _, av := it.Element() |
| key := prefix + strconv.Itoa(i) |
| flatmapValueFromHCL2Value(m, key, av) |
| i++ |
| } |
| m[prefix+"#"] = strconv.Itoa(i) |
| } |
| |
| // HCL2ValueFromFlatmap converts a map compatible with what would be produced |
| // by the "flatmap" package to a HCL2 (really, the cty dynamic types library |
| // that HCL2 uses) object type. |
| // |
| // The intended result type must be provided in order to guide how the |
| // map contents are decoded. This must be an object type or this function |
| // will panic. |
| // |
| // Flatmap values can only represent maps when they are of primitive types, |
| // so the given type must not have any maps of complex types or the result |
| // is undefined. |
| // |
| // The result may contain null values if the given map does not contain keys |
| // for all of the different key paths implied by the given type. |
| func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) { |
| if m == nil { |
| return cty.NullVal(ty), nil |
| } |
| if !ty.IsObjectType() { |
| panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty)) |
| } |
| |
| return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes()) |
| } |
| |
| func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) { |
| var val cty.Value |
| var err error |
| switch { |
| case ty.IsPrimitiveType(): |
| val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty) |
| case ty.IsObjectType(): |
| val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes()) |
| case ty.IsTupleType(): |
| val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes()) |
| case ty.IsMapType(): |
| val, err = hcl2ValueFromFlatmapMap(m, key+".", ty) |
| case ty.IsListType(): |
| val, err = hcl2ValueFromFlatmapList(m, key+".", ty) |
| case ty.IsSetType(): |
| val, err = hcl2ValueFromFlatmapSet(m, key+".", ty) |
| default: |
| err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName()) |
| } |
| |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| return val, nil |
| } |
| |
| func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) { |
| rawVal, exists := m[key] |
| if !exists { |
| return cty.NullVal(ty), nil |
| } |
| if rawVal == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| var err error |
| val := cty.StringVal(rawVal) |
| val, err = convert.Convert(val, ty) |
| if err != nil { |
| // This should never happen for _valid_ input, but flatmap data might |
| // be tampered with by the user and become invalid. |
| return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %s", key, err) |
| } |
| |
| return val, nil |
| } |
| |
| func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) { |
| vals := make(map[string]cty.Value) |
| for name, aty := range atys { |
| val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty) |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| vals[name] = val |
| } |
| return cty.ObjectVal(vals), nil |
| } |
| |
| func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) { |
| var vals []cty.Value |
| |
| // if the container is unknown, there is no count string |
| listName := strings.TrimRight(prefix, ".") |
| if m[listName] == UnknownVariableValue { |
| return cty.UnknownVal(cty.Tuple(etys)), nil |
| } |
| |
| countStr, exists := m[prefix+"#"] |
| if !exists { |
| return cty.NullVal(cty.Tuple(etys)), nil |
| } |
| if countStr == UnknownVariableValue { |
| return cty.UnknownVal(cty.Tuple(etys)), nil |
| } |
| |
| count, err := strconv.Atoi(countStr) |
| if err != nil { |
| return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err) |
| } |
| if count != len(etys) { |
| return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys)) |
| } |
| |
| vals = make([]cty.Value, len(etys)) |
| for i, ety := range etys { |
| key := prefix + strconv.Itoa(i) |
| val, err := hcl2ValueFromFlatmapValue(m, key, ety) |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| vals[i] = val |
| } |
| return cty.TupleVal(vals), nil |
| } |
| |
| func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { |
| vals := make(map[string]cty.Value) |
| ety := ty.ElementType() |
| |
| // if the container is unknown, there is no count string |
| listName := strings.TrimRight(prefix, ".") |
| if m[listName] == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| // We actually don't really care about the "count" of a map for our |
| // purposes here, but we do need to check if it _exists_ in order to |
| // recognize the difference between null (not set at all) and empty. |
| if strCount, exists := m[prefix+"%"]; !exists { |
| return cty.NullVal(ty), nil |
| } else if strCount == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| for fullKey := range m { |
| if !strings.HasPrefix(fullKey, prefix) { |
| continue |
| } |
| |
| // The flatmap format doesn't allow us to distinguish between keys |
| // that contain periods and nested objects, so by convention a |
| // map is only ever of primitive type in flatmap, and we just assume |
| // that the remainder of the raw key (dots and all) is the key we |
| // want in the result value. |
| key := fullKey[len(prefix):] |
| if key == "%" { |
| // Ignore the "count" key |
| continue |
| } |
| |
| val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety) |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| vals[key] = val |
| } |
| |
| if len(vals) == 0 { |
| return cty.MapValEmpty(ety), nil |
| } |
| return cty.MapVal(vals), nil |
| } |
| |
| func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { |
| var vals []cty.Value |
| |
| // if the container is unknown, there is no count string |
| listName := strings.TrimRight(prefix, ".") |
| if m[listName] == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| countStr, exists := m[prefix+"#"] |
| if !exists { |
| return cty.NullVal(ty), nil |
| } |
| if countStr == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| count, err := strconv.Atoi(countStr) |
| if err != nil { |
| return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %s", prefix, err) |
| } |
| |
| ety := ty.ElementType() |
| if count == 0 { |
| return cty.ListValEmpty(ety), nil |
| } |
| |
| vals = make([]cty.Value, count) |
| for i := 0; i < count; i++ { |
| key := prefix + strconv.Itoa(i) |
| val, err := hcl2ValueFromFlatmapValue(m, key, ety) |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| vals[i] = val |
| } |
| |
| return cty.ListVal(vals), nil |
| } |
| |
| func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) { |
| var vals []cty.Value |
| ety := ty.ElementType() |
| |
| // if the container is unknown, there is no count string |
| listName := strings.TrimRight(prefix, ".") |
| if m[listName] == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| strCount, exists := m[prefix+"#"] |
| if !exists { |
| return cty.NullVal(ty), nil |
| } else if strCount == UnknownVariableValue { |
| return cty.UnknownVal(ty), nil |
| } |
| |
| // Keep track of keys we've seen, se we don't add the same set value |
| // multiple times. The cty.Set will normally de-duplicate values, but we may |
| // have unknown values that would not show as equivalent. |
| seen := map[string]bool{} |
| |
| for fullKey := range m { |
| if !strings.HasPrefix(fullKey, prefix) { |
| continue |
| } |
| subKey := fullKey[len(prefix):] |
| if subKey == "#" { |
| // Ignore the "count" key |
| continue |
| } |
| key := fullKey |
| if dot := strings.IndexByte(subKey, '.'); dot != -1 { |
| key = fullKey[:dot+len(prefix)] |
| } |
| |
| if seen[key] { |
| continue |
| } |
| |
| seen[key] = true |
| |
| // The flatmap format doesn't allow us to distinguish between keys |
| // that contain periods and nested objects, so by convention a |
| // map is only ever of primitive type in flatmap, and we just assume |
| // that the remainder of the raw key (dots and all) is the key we |
| // want in the result value. |
| |
| val, err := hcl2ValueFromFlatmapValue(m, key, ety) |
| if err != nil { |
| return cty.DynamicVal, err |
| } |
| vals = append(vals, val) |
| } |
| |
| if len(vals) == 0 && strCount == "1" { |
| // An empty set wouldn't be represented in the flatmap, so this must be |
| // a single empty object since the count is actually 1. |
| // Add an appropriately typed null value to the set. |
| var val cty.Value |
| switch { |
| case ety.IsMapType(): |
| val = cty.MapValEmpty(ety) |
| case ety.IsListType(): |
| val = cty.ListValEmpty(ety) |
| case ety.IsSetType(): |
| val = cty.SetValEmpty(ety) |
| case ety.IsObjectType(): |
| // TODO: cty.ObjectValEmpty |
| objectMap := map[string]cty.Value{} |
| for attr, ty := range ety.AttributeTypes() { |
| objectMap[attr] = cty.NullVal(ty) |
| } |
| val = cty.ObjectVal(objectMap) |
| default: |
| val = cty.NullVal(ety) |
| } |
| vals = append(vals, val) |
| |
| } else if len(vals) == 0 { |
| return cty.SetValEmpty(ety), nil |
| } |
| |
| return cty.SetVal(vals), nil |
| } |