github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/command/meta_vars.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "strings" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hclsyntax" 11 hcljson "github.com/hashicorp/hcl/v2/json" 12 "github.com/hashicorp/terraform/internal/backend" 13 "github.com/hashicorp/terraform/internal/configs" 14 "github.com/hashicorp/terraform/internal/terraform" 15 "github.com/hashicorp/terraform/internal/tfdiags" 16 ) 17 18 // VarEnvPrefix is the prefix for environment variables that represent values 19 // for root module input variables. 20 const VarEnvPrefix = "TF_VAR_" 21 22 // collectVariableValues inspects the various places that root module input variable 23 // values can come from and constructs a map ready to be passed to the 24 // backend as part of a backend.Operation. 25 // 26 // This method returns diagnostics relating to the collection of the values, 27 // but the values themselves may produce additional diagnostics when finally 28 // parsed. 29 func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue, tfdiags.Diagnostics) { 30 var diags tfdiags.Diagnostics 31 ret := map[string]backend.UnparsedVariableValue{} 32 33 // First we'll deal with environment variables, since they have the lowest 34 // precedence. 35 { 36 env := os.Environ() 37 for _, raw := range env { 38 if !strings.HasPrefix(raw, VarEnvPrefix) { 39 continue 40 } 41 raw = raw[len(VarEnvPrefix):] // trim the prefix 42 43 eq := strings.Index(raw, "=") 44 if eq == -1 { 45 // Seems invalid, so we'll ignore it. 46 continue 47 } 48 49 name := raw[:eq] 50 rawVal := raw[eq+1:] 51 52 ret[name] = unparsedVariableValueString{ 53 str: rawVal, 54 name: name, 55 sourceType: terraform.ValueFromEnvVar, 56 } 57 } 58 } 59 60 // Next up we have some implicit files that are loaded automatically 61 // if they are present. There's the original terraform.tfvars 62 // (DefaultVarsFilename) along with the later-added search for all files 63 // ending in .auto.tfvars. 64 if _, err := os.Stat(DefaultVarsFilename); err == nil { 65 moreDiags := m.addVarsFromFile(DefaultVarsFilename, terraform.ValueFromAutoFile, ret) 66 diags = diags.Append(moreDiags) 67 } 68 const defaultVarsFilenameJSON = DefaultVarsFilename + ".json" 69 if _, err := os.Stat(defaultVarsFilenameJSON); err == nil { 70 moreDiags := m.addVarsFromFile(defaultVarsFilenameJSON, terraform.ValueFromAutoFile, ret) 71 diags = diags.Append(moreDiags) 72 } 73 if infos, err := ioutil.ReadDir("."); err == nil { 74 // "infos" is already sorted by name, so we just need to filter it here. 75 for _, info := range infos { 76 name := info.Name() 77 if !isAutoVarFile(name) { 78 continue 79 } 80 moreDiags := m.addVarsFromFile(name, terraform.ValueFromAutoFile, ret) 81 diags = diags.Append(moreDiags) 82 } 83 } 84 85 // Finally we process values given explicitly on the command line, either 86 // as individual literal settings or as additional files to read. 87 for _, rawFlag := range m.variableArgs.AllItems() { 88 switch rawFlag.Name { 89 case "-var": 90 // Value should be in the form "name=value", where value is a 91 // raw string whose interpretation will depend on the variable's 92 // parsing mode. 93 raw := rawFlag.Value 94 eq := strings.Index(raw, "=") 95 if eq == -1 { 96 diags = diags.Append(tfdiags.Sourceless( 97 tfdiags.Error, 98 "Invalid -var option", 99 fmt.Sprintf("The given -var option %q is not correctly specified. Must be a variable name and value separated by an equals sign, like -var=\"key=value\".", raw), 100 )) 101 continue 102 } 103 name := raw[:eq] 104 rawVal := raw[eq+1:] 105 if strings.HasSuffix(name, " ") { 106 diags = diags.Append(tfdiags.Sourceless( 107 tfdiags.Error, 108 "Invalid -var option", 109 fmt.Sprintf("Variable name %q is invalid due to trailing space. Did you mean -var=\"%s=%s\"?", name, strings.TrimSuffix(name, " "), strings.TrimPrefix(rawVal, " ")), 110 )) 111 continue 112 } 113 ret[name] = unparsedVariableValueString{ 114 str: rawVal, 115 name: name, 116 sourceType: terraform.ValueFromCLIArg, 117 } 118 119 case "-var-file": 120 moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromNamedFile, ret) 121 diags = diags.Append(moreDiags) 122 123 default: 124 // Should never happen; always a bug in the code that built up 125 // the contents of m.variableArgs. 126 diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) 127 } 128 } 129 130 return ret, diags 131 } 132 133 func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSourceType, to map[string]backend.UnparsedVariableValue) tfdiags.Diagnostics { 134 var diags tfdiags.Diagnostics 135 136 src, err := ioutil.ReadFile(filename) 137 if err != nil { 138 if os.IsNotExist(err) { 139 diags = diags.Append(tfdiags.Sourceless( 140 tfdiags.Error, 141 "Failed to read variables file", 142 fmt.Sprintf("Given variables file %s does not exist.", filename), 143 )) 144 } else { 145 diags = diags.Append(tfdiags.Sourceless( 146 tfdiags.Error, 147 "Failed to read variables file", 148 fmt.Sprintf("Error while reading %s: %s.", filename, err), 149 )) 150 } 151 return diags 152 } 153 154 loader, err := m.initConfigLoader() 155 if err != nil { 156 diags = diags.Append(err) 157 return diags 158 } 159 160 // Record the file source code for snippets in diagnostic messages. 161 loader.Parser().ForceFileSource(filename, src) 162 163 var f *hcl.File 164 if strings.HasSuffix(filename, ".json") { 165 var hclDiags hcl.Diagnostics 166 f, hclDiags = hcljson.Parse(src, filename) 167 diags = diags.Append(hclDiags) 168 if f == nil || f.Body == nil { 169 return diags 170 } 171 } else { 172 var hclDiags hcl.Diagnostics 173 f, hclDiags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1}) 174 diags = diags.Append(hclDiags) 175 if f == nil || f.Body == nil { 176 return diags 177 } 178 } 179 180 // Before we do our real decode, we'll probe to see if there are any blocks 181 // of type "variable" in this body, since it's a common mistake for new 182 // users to put variable declarations in tfvars rather than variable value 183 // definitions, and otherwise our error message for that case is not so 184 // helpful. 185 { 186 content, _, _ := f.Body.PartialContent(&hcl.BodySchema{ 187 Blocks: []hcl.BlockHeaderSchema{ 188 { 189 Type: "variable", 190 LabelNames: []string{"name"}, 191 }, 192 }, 193 }) 194 for _, block := range content.Blocks { 195 name := block.Labels[0] 196 diags = diags.Append(&hcl.Diagnostic{ 197 Severity: hcl.DiagError, 198 Summary: "Variable declaration in .tfvars file", 199 Detail: fmt.Sprintf("A .tfvars file is used to assign values to variables that have already been declared in .tf files, not to declare new variables. To declare variable %q, place this block in one of your .tf files, such as variables.tf.\n\nTo set a value for this variable in %s, use the definition syntax instead:\n %s = <value>", name, block.TypeRange.Filename, name), 200 Subject: &block.TypeRange, 201 }) 202 } 203 if diags.HasErrors() { 204 // If we already found problems then JustAttributes below will find 205 // the same problems with less-helpful messages, so we'll bail for 206 // now to let the user focus on the immediate problem. 207 return diags 208 } 209 } 210 211 attrs, hclDiags := f.Body.JustAttributes() 212 diags = diags.Append(hclDiags) 213 214 for name, attr := range attrs { 215 to[name] = unparsedVariableValueExpression{ 216 expr: attr.Expr, 217 sourceType: sourceType, 218 } 219 } 220 return diags 221 } 222 223 // unparsedVariableValueLiteral is a backend.UnparsedVariableValue 224 // implementation that was actually already parsed (!). This is 225 // intended to deal with expressions inside "tfvars" files. 226 type unparsedVariableValueExpression struct { 227 expr hcl.Expression 228 sourceType terraform.ValueSourceType 229 } 230 231 func (v unparsedVariableValueExpression) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 232 var diags tfdiags.Diagnostics 233 val, hclDiags := v.expr.Value(nil) // nil because no function calls or variable references are allowed here 234 diags = diags.Append(hclDiags) 235 236 rng := tfdiags.SourceRangeFromHCL(v.expr.Range()) 237 238 return &terraform.InputValue{ 239 Value: val, 240 SourceType: v.sourceType, 241 SourceRange: rng, 242 }, diags 243 } 244 245 // unparsedVariableValueString is a backend.UnparsedVariableValue 246 // implementation that parses its value from a string. This can be used 247 // to deal with values given directly on the command line and via environment 248 // variables. 249 type unparsedVariableValueString struct { 250 str string 251 name string 252 sourceType terraform.ValueSourceType 253 } 254 255 func (v unparsedVariableValueString) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 256 var diags tfdiags.Diagnostics 257 258 val, hclDiags := mode.Parse(v.name, v.str) 259 diags = diags.Append(hclDiags) 260 261 return &terraform.InputValue{ 262 Value: val, 263 SourceType: v.sourceType, 264 }, diags 265 }