github.com/renier/terraform@v0.7.8-0.20161024133817-eb8a9ef5471a/command/flag_kv.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"regexp"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/hcl"
    11  	"github.com/mitchellh/go-homedir"
    12  )
    13  
    14  // FlagTypedKVis a flag.Value implementation for parsing user variables
    15  // from the command-line in the format of '-var key=value', where value is
    16  // a type intended for use as a Terraform variable
    17  type FlagTypedKV map[string]interface{}
    18  
    19  func (v *FlagTypedKV) String() string {
    20  	return ""
    21  }
    22  
    23  func (v *FlagTypedKV) Set(raw string) error {
    24  	key, value, err := parseVarFlagAsHCL(raw)
    25  	if err != nil {
    26  		return err
    27  	}
    28  
    29  	if *v == nil {
    30  		*v = make(map[string]interface{})
    31  	}
    32  
    33  	(*v)[key] = value
    34  	return nil
    35  }
    36  
    37  // FlagStringKV is a flag.Value implementation for parsing user variables
    38  // from the command-line in the format of '-var key=value', where value is
    39  // only ever a primitive.
    40  type FlagStringKV map[string]string
    41  
    42  func (v *FlagStringKV) String() string {
    43  	return ""
    44  }
    45  
    46  func (v *FlagStringKV) Set(raw string) error {
    47  	idx := strings.Index(raw, "=")
    48  	if idx == -1 {
    49  		return fmt.Errorf("No '=' value in arg: %s", raw)
    50  	}
    51  
    52  	if *v == nil {
    53  		*v = make(map[string]string)
    54  	}
    55  
    56  	key, value := raw[0:idx], raw[idx+1:]
    57  	(*v)[key] = value
    58  	return nil
    59  }
    60  
    61  // FlagKVFile is a flag.Value implementation for parsing user variables
    62  // from the command line in the form of files. i.e. '-var-file=foo'
    63  type FlagKVFile map[string]interface{}
    64  
    65  func (v *FlagKVFile) String() string {
    66  	return ""
    67  }
    68  
    69  func (v *FlagKVFile) Set(raw string) error {
    70  	vs, err := loadKVFile(raw)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	if *v == nil {
    76  		*v = make(map[string]interface{})
    77  	}
    78  
    79  	for key, value := range vs {
    80  		(*v)[key] = value
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  func loadKVFile(rawPath string) (map[string]interface{}, error) {
    87  	path, err := homedir.Expand(rawPath)
    88  	if err != nil {
    89  		return nil, fmt.Errorf(
    90  			"Error expanding path: %s", err)
    91  	}
    92  
    93  	// Read the HCL file and prepare for parsing
    94  	d, err := ioutil.ReadFile(path)
    95  	if err != nil {
    96  		return nil, fmt.Errorf(
    97  			"Error reading %s: %s", path, err)
    98  	}
    99  
   100  	// Parse it
   101  	obj, err := hcl.Parse(string(d))
   102  	if err != nil {
   103  		return nil, fmt.Errorf(
   104  			"Error parsing %s: %s", path, err)
   105  	}
   106  
   107  	var result map[string]interface{}
   108  	if err := hcl.DecodeObject(&result, obj); err != nil {
   109  		return nil, fmt.Errorf(
   110  			"Error decoding Terraform vars file: %s\n\n"+
   111  				"The vars file should be in the format of `key = \"value\"`.\n"+
   112  				"Decoding errors are usually caused by an invalid format.",
   113  			err)
   114  	}
   115  	err = flattenMultiMaps(result)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	return result, nil
   121  }
   122  
   123  // FlagStringSlice is a flag.Value implementation for parsing targets from the
   124  // command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar
   125  
   126  type FlagStringSlice []string
   127  
   128  func (v *FlagStringSlice) String() string {
   129  	return ""
   130  }
   131  func (v *FlagStringSlice) Set(raw string) error {
   132  	*v = append(*v, raw)
   133  
   134  	return nil
   135  }
   136  
   137  var (
   138  	// This regular expression is how we check if a value for a variable
   139  	// matches what we'd expect a rich HCL value to be. For example: {
   140  	// definitely signals a map. If a value DOESN'T match this, we return
   141  	// it as a raw string.
   142  	varFlagHCLRe = regexp.MustCompile(`^["\[\{]`)
   143  )
   144  
   145  // parseVarFlagAsHCL parses the value of a single variable as would have been specified
   146  // on the command line via -var or in an environment variable named TF_VAR_x, where x is
   147  // the name of the variable. In order to get around the restriction of HCL requiring a
   148  // top level object, we prepend a sentinel key, decode the user-specified value as its
   149  // value and pull the value back out of the resulting map.
   150  func parseVarFlagAsHCL(input string) (string, interface{}, error) {
   151  	idx := strings.Index(input, "=")
   152  	if idx == -1 {
   153  		return "", nil, fmt.Errorf("No '=' value in variable: %s", input)
   154  	}
   155  	probablyName := input[0:idx]
   156  	value := input[idx+1:]
   157  	trimmed := strings.TrimSpace(value)
   158  
   159  	// If the value is a simple number, don't parse it as hcl because the
   160  	// variable type may actually be a string, and HCL will convert it to the
   161  	// numberic value. We could check this in the validation later, but the
   162  	// conversion may alter the string value.
   163  	if _, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
   164  		return probablyName, value, nil
   165  	}
   166  	if _, err := strconv.ParseFloat(trimmed, 64); err == nil {
   167  		return probablyName, value, nil
   168  	}
   169  	// HCL will also parse hex as a number
   170  	if strings.HasPrefix(trimmed, "0x") {
   171  		if _, err := strconv.ParseInt(trimmed[2:], 16, 64); err == nil {
   172  			return probablyName, value, nil
   173  		}
   174  	}
   175  
   176  	parsed, err := hcl.Parse(input)
   177  	if err != nil {
   178  		// If it didn't parse as HCL, we check if it doesn't match our
   179  		// whitelist of TF-accepted HCL types for inputs. If not, then
   180  		// we let it through as a raw string.
   181  		if !varFlagHCLRe.MatchString(trimmed) {
   182  			return probablyName, value, nil
   183  		}
   184  
   185  		// This covers flags of the form `foo=bar` which is not valid HCL
   186  		// At this point, probablyName is actually the name, and the remainder
   187  		// of the expression after the equals sign is the value.
   188  		if regexp.MustCompile(`Unknown token: \d+:\d+ IDENT`).Match([]byte(err.Error())) {
   189  			return probablyName, value, nil
   190  		}
   191  
   192  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
   193  	}
   194  
   195  	var decoded map[string]interface{}
   196  	if hcl.DecodeObject(&decoded, parsed); err != nil {
   197  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
   198  	}
   199  
   200  	// Cover cases such as key=
   201  	if len(decoded) == 0 {
   202  		return probablyName, "", nil
   203  	}
   204  
   205  	if len(decoded) > 1 {
   206  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", probablyName, input)
   207  	}
   208  
   209  	err = flattenMultiMaps(decoded)
   210  	if err != nil {
   211  		return probablyName, "", err
   212  	}
   213  
   214  	var k string
   215  	var v interface{}
   216  	for k, v = range decoded {
   217  		break
   218  	}
   219  	return k, v, nil
   220  }
   221  
   222  // Variables don't support any type that can be configured via multiple
   223  // declarations of the same HCL map, so any instances of
   224  // []map[string]interface{} are either a single map that can be flattened, or
   225  // are invalid config.
   226  func flattenMultiMaps(m map[string]interface{}) error {
   227  	for k, v := range m {
   228  		switch v := v.(type) {
   229  		case []map[string]interface{}:
   230  			switch {
   231  			case len(v) > 1:
   232  				return fmt.Errorf("multiple map declarations not supported for variables")
   233  			case len(v) == 1:
   234  				m[k] = v[0]
   235  			}
   236  		}
   237  	}
   238  	return nil
   239  }