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