diff --git a/command/apply.go b/command/apply.go index 36a8d5cae8..1437dc1aba 100644 --- a/command/apply.go +++ b/command/apply.go @@ -179,6 +179,15 @@ func (c *ApplyCommand) Run(args []string) int { c.showDiagnostics(err) return 1 } + { + var moreDiags tfdiags.Diagnostics + opReq.Variables, moreDiags = c.collectVariableValues() + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + } op, err := c.RunOperation(be, opReq) if err != nil { diff --git a/command/meta_config.go b/command/meta_config.go index 5a7385a311..fb3491b6c1 100644 --- a/command/meta_config.go +++ b/command/meta_config.go @@ -210,27 +210,6 @@ func (m *Meta) initDirFromModule(targetDir string, addr string, hooks configload return diags } -// loadVarsFile reads a file from the given path and interprets it as a -// "vars file", returning the contained values as a map. -// -// The file is read using the parser associated with the receiver's -// configuration loader, which means that the file's contents will be added -// to the source cache that is used for config snippets in diagnostic messages. -func (m *Meta) loadVarsFile(filename string) (map[string]cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - loader, err := m.initConfigLoader() - if err != nil { - diags = diags.Append(err) - return nil, diags - } - - parser := loader.Parser() - ret, hclDiags := parser.LoadValuesFile(filename) - diags = diags.Append(hclDiags) - return ret, diags -} - // inputForSchema uses interactive prompts to try to populate any // not-yet-populated required attributes in the given object value to // comply with the given schema. diff --git a/command/meta_vars.go b/command/meta_vars.go new file mode 100644 index 0000000000..ff68d50e43 --- /dev/null +++ b/command/meta_vars.go @@ -0,0 +1,219 @@ +package command + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + hcljson "github.com/hashicorp/hcl2/hcl/json" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" +) + +// collectVariableValues inspects the various places that root module input variable +// values can come from and constructs a map ready to be passed to the +// backend as part of a backend.Operation. +// +// This method returns diagnostics relating to the collection of the values, +// but the values themselves may produce additional diagnostics when finally +// parsed. +func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := map[string]backend.UnparsedVariableValue{} + + // First we'll deal with environment variables, since they have the lowest + // precedence. + { + env := os.Environ() + for _, raw := range env { + if !strings.HasPrefix(raw, terraform.VarEnvPrefix) { + continue + } + raw = raw[len(terraform.VarEnvPrefix):] // trim the prefix + + eq := strings.Index(raw, "=") + if eq == -1 { + // Seems invalid, so we'll ignore it. + continue + } + + name := raw[:eq] + rawVal := raw[eq+1:] + + ret[name] = unparsedVariableValueString{ + str: rawVal, + name: name, + sourceType: terraform.ValueFromEnvVar, + } + } + } + + // Next up we have some implicit files that are loaded automatically + // if they are present. There's the original terraform.tfvars + // (DefaultVarsFilename) along with the later-added search for all files + // ending in .auto.tfvars. + if _, err := os.Stat(DefaultVarsFilename); err == nil { + moreDiags := m.addVarsFromFile(DefaultVarsFilename, terraform.ValueFromFile, ret) + diags = diags.Append(moreDiags) + } + if infos, err := ioutil.ReadDir("."); err == nil { + // "infos" is already sorted by name, so we just need to filter it here. + for _, info := range infos { + name := info.Name() + if !isAutoVarFile(name) { + continue + } + moreDiags := m.addVarsFromFile(name, terraform.ValueFromFile, ret) + diags = diags.Append(moreDiags) + } + } + + // Finally we process values given explicitly on the command line, either + // as individual literal settings or as additional files to read. + for _, rawFlag := range m.variableArgs.AllItems() { + switch rawFlag.Name { + case "-var": + // Value should be in the form "name=value", where value is a + // raw string whose interpretation will depend on the variable's + // parsing mode. + raw := rawFlag.Value + eq := strings.Index(raw, "=") + if eq == -1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid -var option", + 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), + )) + continue + } + name := raw[:eq] + rawVal := raw[eq+1:] + ret[name] = unparsedVariableValueString{ + str: rawVal, + name: name, + sourceType: terraform.ValueFromCLIArg, + } + + case "-var-file": + moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromFile, ret) + diags = diags.Append(moreDiags) + + default: + // Should never happen; always a bug in the code that built up + // the contents of m.variableArgs. + diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) + } + } + + return ret, diags +} + + +func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSourceType, to map[string]backend.UnparsedVariableValue) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + src, err := ioutil.ReadFile(filename) + if err != nil { + if os.IsNotExist(err) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read variables file", + fmt.Sprintf("Given variables file %s does not exist.", filename), + )) + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read variables file", + fmt.Sprintf("Error while reading %s: %s.", filename, err), + )) + } + return diags + } + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return diags + } + + // Record the file source code for snippets in diagnostic messages. + loader.Parser().ForceFileSource(filename, src) + + var f *hcl.File + if strings.HasSuffix(filename, ".json") { + var hclDiags hcl.Diagnostics + f, hclDiags = hcljson.Parse(src, filename) + diags = diags.Append(hclDiags) + if f == nil || f.Body == nil { + return diags + } + } else { + var hclDiags hcl.Diagnostics + f, hclDiags = hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(hclDiags) + if f == nil || f.Body == nil { + return diags + } + } + + attrs, hclDiags := f.Body.JustAttributes() + diags = diags.Append(hclDiags) + + for name, attr := range attrs { + to[name] = unparsedVariableValueExpression{ + expr: attr.Expr, + sourceType: sourceType, + } + } + return diags +} + +// unparsedVariableValueLiteral is a backend.UnparsedVariableValue +// implementation that was actually already parsed (!). This is +// intended to deal with expressions inside "tfvars" files. +type unparsedVariableValueExpression struct { + expr hcl.Expression + sourceType terraform.ValueSourceType +} + +func (v unparsedVariableValueExpression) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + val, hclDiags := v.expr.Value(nil) // nil because no function calls or variable references are allowed here + diags = diags.Append(hclDiags) + + rng := tfdiags.SourceRangeFromHCL(v.expr.Range()) + + return &terraform.InputValue{ + Value: val, + SourceType: v.sourceType, + SourceRange: rng, + }, diags +} + +// unparsedVariableValueString is a backend.UnparsedVariableValue +// implementation that parses its value from a string. This can be used +// to deal with values given directly on the command line and via environment +// variables. +type unparsedVariableValueString struct { + str string + name string + sourceType terraform.ValueSourceType +} + +func (v unparsedVariableValueString) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + val, hclDiags := mode.Parse(v.name, v.str) + diags = diags.Append(hclDiags) + + return &terraform.InputValue{ + Value: val, + SourceType: v.sourceType, + }, diags +} diff --git a/command/plan.go b/command/plan.go index 66a961070d..6f45650a8b 100644 --- a/command/plan.go +++ b/command/plan.go @@ -108,6 +108,15 @@ func (c *PlanCommand) Run(args []string) int { c.showDiagnostics(err) return 1 } + { + var moreDiags tfdiags.Diagnostics + opReq.Variables, moreDiags = c.collectVariableValues() + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + } // c.Backend above has a non-obvious side-effect of also populating // c.backendState, which is the state-shaped formulation of the effective diff --git a/command/refresh.go b/command/refresh.go index e3e5c3f90c..d083111c4e 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -78,6 +78,15 @@ func (c *RefreshCommand) Run(args []string) int { c.showDiagnostics(err) return 1 } + { + var moreDiags tfdiags.Diagnostics + opReq.Variables, moreDiags = c.collectVariableValues() + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + } op, err := c.RunOperation(b, opReq) if err != nil {