github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/backend" 13 "github.com/hashicorp/terraform/configs" 14 "github.com/hashicorp/terraform/terraform" 15 "github.com/hashicorp/terraform/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 ret[name] = unparsedVariableValueString{ 106 str: rawVal, 107 name: name, 108 sourceType: terraform.ValueFromCLIArg, 109 } 110 111 case "-var-file": 112 moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromNamedFile, ret) 113 diags = diags.Append(moreDiags) 114 115 default: 116 // Should never happen; always a bug in the code that built up 117 // the contents of m.variableArgs. 118 diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) 119 } 120 } 121 122 return ret, diags 123 } 124 125 func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSourceType, to map[string]backend.UnparsedVariableValue) tfdiags.Diagnostics { 126 var diags tfdiags.Diagnostics 127 128 src, err := ioutil.ReadFile(filename) 129 if err != nil { 130 if os.IsNotExist(err) { 131 diags = diags.Append(tfdiags.Sourceless( 132 tfdiags.Error, 133 "Failed to read variables file", 134 fmt.Sprintf("Given variables file %s does not exist.", filename), 135 )) 136 } else { 137 diags = diags.Append(tfdiags.Sourceless( 138 tfdiags.Error, 139 "Failed to read variables file", 140 fmt.Sprintf("Error while reading %s: %s.", filename, err), 141 )) 142 } 143 return diags 144 } 145 146 loader, err := m.initConfigLoader() 147 if err != nil { 148 diags = diags.Append(err) 149 return diags 150 } 151 152 // Record the file source code for snippets in diagnostic messages. 153 loader.Parser().ForceFileSource(filename, src) 154 155 var f *hcl.File 156 if strings.HasSuffix(filename, ".json") { 157 var hclDiags hcl.Diagnostics 158 f, hclDiags = hcljson.Parse(src, filename) 159 diags = diags.Append(hclDiags) 160 if f == nil || f.Body == nil { 161 return diags 162 } 163 } else { 164 var hclDiags hcl.Diagnostics 165 f, hclDiags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1}) 166 diags = diags.Append(hclDiags) 167 if f == nil || f.Body == nil { 168 return diags 169 } 170 } 171 172 // Before we do our real decode, we'll probe to see if there are any blocks 173 // of type "variable" in this body, since it's a common mistake for new 174 // users to put variable declarations in tfvars rather than variable value 175 // definitions, and otherwise our error message for that case is not so 176 // helpful. 177 { 178 content, _, _ := f.Body.PartialContent(&hcl.BodySchema{ 179 Blocks: []hcl.BlockHeaderSchema{ 180 { 181 Type: "variable", 182 LabelNames: []string{"name"}, 183 }, 184 }, 185 }) 186 for _, block := range content.Blocks { 187 name := block.Labels[0] 188 diags = diags.Append(&hcl.Diagnostic{ 189 Severity: hcl.DiagError, 190 Summary: "Variable declaration in .tfvars file", 191 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), 192 Subject: &block.TypeRange, 193 }) 194 } 195 if diags.HasErrors() { 196 // If we already found problems then JustAttributes below will find 197 // the same problems with less-helpful messages, so we'll bail for 198 // now to let the user focus on the immediate problem. 199 return diags 200 } 201 } 202 203 attrs, hclDiags := f.Body.JustAttributes() 204 diags = diags.Append(hclDiags) 205 206 for name, attr := range attrs { 207 to[name] = unparsedVariableValueExpression{ 208 expr: attr.Expr, 209 sourceType: sourceType, 210 } 211 } 212 return diags 213 } 214 215 // unparsedVariableValueLiteral is a backend.UnparsedVariableValue 216 // implementation that was actually already parsed (!). This is 217 // intended to deal with expressions inside "tfvars" files. 218 type unparsedVariableValueExpression struct { 219 expr hcl.Expression 220 sourceType terraform.ValueSourceType 221 } 222 223 func (v unparsedVariableValueExpression) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 224 var diags tfdiags.Diagnostics 225 val, hclDiags := v.expr.Value(nil) // nil because no function calls or variable references are allowed here 226 diags = diags.Append(hclDiags) 227 228 rng := tfdiags.SourceRangeFromHCL(v.expr.Range()) 229 230 return &terraform.InputValue{ 231 Value: val, 232 SourceType: v.sourceType, 233 SourceRange: rng, 234 }, diags 235 } 236 237 // unparsedVariableValueString is a backend.UnparsedVariableValue 238 // implementation that parses its value from a string. This can be used 239 // to deal with values given directly on the command line and via environment 240 // variables. 241 type unparsedVariableValueString struct { 242 str string 243 name string 244 sourceType terraform.ValueSourceType 245 } 246 247 func (v unparsedVariableValueString) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { 248 var diags tfdiags.Diagnostics 249 250 val, hclDiags := mode.Parse(v.name, v.str) 251 diags = diags.Append(hclDiags) 252 253 return &terraform.InputValue{ 254 Value: val, 255 SourceType: v.sourceType, 256 }, diags 257 }