github.com/pulumi/terraform@v1.4.0/pkg/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/pulumi/terraform/pkg/backend"
    13  	"github.com/pulumi/terraform/pkg/configs"
    14  	"github.com/pulumi/terraform/pkg/terraform"
    15  	"github.com/pulumi/terraform/pkg/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  }