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  }