github.com/ewbankkit/terraform@v0.7.7/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 }