github.com/nov1n/terraform@v0.7.9-0.20161103151050-bf6852f38e28/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  
   170  	// HCL will also parse hex as a number
   171  	if strings.HasPrefix(trimmed, "0x") {
   172  		if _, err := strconv.ParseInt(trimmed[2:], 16, 64); err == nil {
   173  			return probablyName, value, nil
   174  		}
   175  	}
   176  
   177  	// If the value is a boolean value, also convert it to a simple string
   178  	// since Terraform core doesn't accept primitives as anything other
   179  	// than string for now.
   180  	if _, err := strconv.ParseBool(trimmed); err == nil {
   181  		return probablyName, value, nil
   182  	}
   183  
   184  	parsed, err := hcl.Parse(input)
   185  	if err != nil {
   186  		// If it didn't parse as HCL, we check if it doesn't match our
   187  		// whitelist of TF-accepted HCL types for inputs. If not, then
   188  		// we let it through as a raw string.
   189  		if !varFlagHCLRe.MatchString(trimmed) {
   190  			return probablyName, value, nil
   191  		}
   192  
   193  		// This covers flags of the form `foo=bar` which is not valid HCL
   194  		// At this point, probablyName is actually the name, and the remainder
   195  		// of the expression after the equals sign is the value.
   196  		if regexp.MustCompile(`Unknown token: \d+:\d+ IDENT`).Match([]byte(err.Error())) {
   197  			return probablyName, value, nil
   198  		}
   199  
   200  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
   201  	}
   202  
   203  	var decoded map[string]interface{}
   204  	if hcl.DecodeObject(&decoded, parsed); err != nil {
   205  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL: %s", probablyName, input, err)
   206  	}
   207  
   208  	// Cover cases such as key=
   209  	if len(decoded) == 0 {
   210  		return probablyName, "", nil
   211  	}
   212  
   213  	if len(decoded) > 1 {
   214  		return "", nil, fmt.Errorf("Cannot parse value for variable %s (%q) as valid HCL. Only one value may be specified.", probablyName, input)
   215  	}
   216  
   217  	err = flattenMultiMaps(decoded)
   218  	if err != nil {
   219  		return probablyName, "", err
   220  	}
   221  
   222  	var k string
   223  	var v interface{}
   224  	for k, v = range decoded {
   225  		break
   226  	}
   227  	return k, v, nil
   228  }
   229  
   230  // Variables don't support any type that can be configured via multiple
   231  // declarations of the same HCL map, so any instances of
   232  // []map[string]interface{} are either a single map that can be flattened, or
   233  // are invalid config.
   234  func flattenMultiMaps(m map[string]interface{}) error {
   235  	for k, v := range m {
   236  		switch v := v.(type) {
   237  		case []map[string]interface{}:
   238  			switch {
   239  			case len(v) > 1:
   240  				return fmt.Errorf("multiple map declarations not supported for variables")
   241  			case len(v) == 1:
   242  				m[k] = v[0]
   243  			}
   244  		}
   245  	}
   246  	return nil
   247  }