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 }