terraformutils/flatmap.go (309 lines of code) (raw):
// Copyright 2018 The Terraformer Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package terraformutils
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/zclconf/go-cty/cty"
)
type Flatmapper interface {
Parse(ty cty.Type) (map[string]interface{}, error)
}
type FlatmapParser struct {
Flatmapper
attributes map[string]string
ignoreKeys []*regexp.Regexp
allowEmptyValues []*regexp.Regexp
}
func NewFlatmapParser(attributes map[string]string, ignoreKeys []*regexp.Regexp, allowEmptyValues []*regexp.Regexp) *FlatmapParser {
return &FlatmapParser{
attributes: attributes,
ignoreKeys: ignoreKeys,
allowEmptyValues: allowEmptyValues,
}
}
// FromFlatmap converts a map compatible with what would be produced
// by the "flatmap" package to a map[string]interface{} 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 (p *FlatmapParser) Parse(ty cty.Type) (map[string]interface{}, error) {
if p.attributes == nil {
return nil, nil
}
if !ty.IsObjectType() {
return nil, fmt.Errorf("FlatmapParser#Parse called on %#v", ty)
}
return p.fromFlatmapObject("", ty.AttributeTypes())
}
func (p *FlatmapParser) fromFlatmapValue(key string, ty cty.Type) (interface{}, error) {
switch {
case ty.IsPrimitiveType():
return p.fromFlatmapPrimitive(key)
case ty.IsObjectType():
return p.fromFlatmapObject(key+".", ty.AttributeTypes())
case ty.IsTupleType():
return p.fromFlatmapTuple(key+".", ty.TupleElementTypes())
case ty.IsMapType():
return p.fromFlatmapMap(key+".", ty.ElementType())
case ty.IsListType():
return p.fromFlatmapList(key+".", ty.ElementType())
case ty.IsSetType():
return p.fromFlatmapSet(key+".", ty.ElementType())
default:
return nil, fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName())
}
}
func (p *FlatmapParser) fromFlatmapPrimitive(key string) (interface{}, error) {
value, ok := p.attributes[key]
if !ok {
return nil, nil
}
return value, nil
}
func (p *FlatmapParser) fromFlatmapObject(prefix string, tys map[string]cty.Type) (map[string]interface{}, error) {
values := make(map[string]interface{})
for name, ty := range tys {
inAttributes := false
attributeName := ""
for k := range p.attributes {
if k == prefix+name {
attributeName = k
inAttributes = true
break
}
if k == name {
attributeName = k
inAttributes = true
break
}
if strings.HasPrefix(k, prefix+name+".") {
attributeName = k
inAttributes = true
break
}
lastAttribute := (prefix + name)[len(prefix):]
if lastAttribute == k {
attributeName = k
inAttributes = true
break
}
}
if _, exist := p.attributes[prefix+name+".#"]; exist {
attributeName = prefix + name + ".#"
inAttributes = true
}
if _, exist := p.attributes[prefix+name+".%"]; exist {
attributeName = prefix + name + ".%"
inAttributes = true
}
if !inAttributes {
continue
}
if p.isAttributeIgnored(prefix + name) {
continue
}
value, err := p.fromFlatmapValue(prefix+name, ty)
if err != nil {
return nil, err
}
if p.isValueAllowed(value, attributeName) {
values[name] = value
}
}
if len(values) == 0 {
return nil, nil
}
return values, nil
}
func (p *FlatmapParser) fromFlatmapTuple(prefix string, tys []cty.Type) ([]interface{}, error) {
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if p.attributes[listName] == hcl2shim.UnknownVariableValue {
return nil, nil
}
countStr, exists := p.attributes[prefix+"#"]
if !exists {
return nil, nil
}
if countStr == hcl2shim.UnknownVariableValue {
return nil, nil
}
count, err := strconv.Atoi(countStr)
if err != nil {
return nil, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
}
if count != len(tys) {
return nil, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(tys))
}
var values []interface{}
for i, ty := range tys {
key := prefix + strconv.Itoa(i)
value, err := p.fromFlatmapValue(key, ty)
if err != nil {
return nil, err
}
if p.isValueAllowed(value, prefix) {
values = append(values, value)
}
}
if len(values) == 0 {
return nil, nil
}
return values, nil
}
func (p *FlatmapParser) fromFlatmapMap(prefix string, ty cty.Type) (map[string]interface{}, error) {
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if p.attributes[listName] == hcl2shim.UnknownVariableValue {
return nil, 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.
strCount, exists := p.attributes[prefix+"%"]
if !exists {
return nil, nil
}
if strCount == hcl2shim.UnknownVariableValue {
return nil, nil
}
values := make(map[string]interface{})
for fullKey := range p.attributes {
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
}
if p.isAttributeIgnored(fullKey) {
continue
}
value, err := p.fromFlatmapValue(fullKey, ty)
if err != nil {
return nil, err
}
if p.isValueAllowed(value, prefix) {
values[key] = value
}
}
if len(values) == 0 {
return nil, nil
}
return values, nil
}
func (p *FlatmapParser) fromFlatmapList(prefix string, ty cty.Type) ([]interface{}, error) {
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if p.attributes[listName] == hcl2shim.UnknownVariableValue {
return nil, nil
}
countStr, exists := p.attributes[prefix+"#"]
if !exists {
return nil, nil
}
if countStr == hcl2shim.UnknownVariableValue {
return nil, nil
}
count, err := strconv.Atoi(countStr)
if err != nil {
return nil, fmt.Errorf("invalid count value for %q in state: %s", prefix, err)
}
if count == 0 {
return nil, nil
}
var values []interface{}
for i := 0; i < count; i++ {
key := prefix + strconv.Itoa(i)
if p.isAttributeIgnored(key) {
continue
}
value, err := p.fromFlatmapValue(key, ty)
if err != nil {
return nil, err
}
if p.isValueAllowed(value, prefix) {
values = append(values, value)
}
}
return values, nil
}
func (p *FlatmapParser) fromFlatmapSet(prefix string, ty cty.Type) ([]interface{}, error) {
// if the container is unknown, there is no count string
listName := strings.TrimRight(prefix, ".")
if p.attributes[listName] == hcl2shim.UnknownVariableValue {
return nil, nil
}
strCount, exists := p.attributes[prefix+"#"]
if !exists {
return nil, nil
}
if strCount == hcl2shim.UnknownVariableValue {
return nil, 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{}
var values []interface{}
for fullKey := range p.attributes {
if !strings.HasPrefix(fullKey, prefix) {
continue
}
subKey := fullKey[len(prefix):]
if subKey == "#" {
// Ignore the "count" key
continue
}
key := fullKey
if p.isAttributeIgnored(fullKey) {
continue
}
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.
value, err := p.fromFlatmapValue(key, ty)
if err != nil {
return nil, err
}
if p.isValueAllowed(value, prefix) {
values = append(values, value)
}
}
if len(values) == 0 {
return nil, nil
}
return values, nil
}
func (p *FlatmapParser) isAttributeIgnored(name string) bool {
ignored := false
for _, pattern := range p.ignoreKeys {
if pattern.MatchString(name) {
ignored = true
break
}
}
return ignored
}
func (p *FlatmapParser) isValueAllowed(value interface{}, prefix string) bool {
if !reflect.ValueOf(value).IsValid() {
return false
}
switch reflect.ValueOf(value).Kind() {
case reflect.Slice:
if reflect.ValueOf(value).Len() == 0 {
return false
}
for i := 0; i < reflect.ValueOf(value).Len(); i++ {
if !reflect.ValueOf(value).Index(i).IsZero() {
return true
}
}
case reflect.Map:
if reflect.ValueOf(value).Len() == 0 {
return false
}
}
if !reflect.ValueOf(value).IsZero() {
return true
}
allowed := false
for _, pattern := range p.allowEmptyValues {
if pattern.MatchString(prefix) {
allowed = true
break
}
}
return allowed
}