mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
39e609d5fd
Previously we were using the experimental HCL 2 repository, but now we'll shift over to the v2 import path within the main HCL repository as part of actually releasing HCL 2.0 as stable. This is a mechanical search/replace to the new import paths. It also switches to the v2.0.0 release of HCL, which includes some new code that Terraform didn't previously have but should not change any behavior that matters for Terraform's purposes. For the moment the experimental HCL2 repository is still an indirect dependency via terraform-config-inspect, so it remains in our go.sum and vendor directories for the moment. Because terraform-config-inspect uses a much smaller subset of the HCL2 functionality, this does still manage to prune the vendor directory a little. A subsequent release of terraform-config-inspect should allow us to completely remove that old repository in a future commit.
258 lines
8.1 KiB
Go
258 lines
8.1 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
hcljson "github.com/hashicorp/hcl/v2/json"
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/configs"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
// VarEnvPrefix is the prefix for environment variables that represent values
|
|
// for root module input variables.
|
|
const VarEnvPrefix = "TF_VAR_"
|
|
|
|
// 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, VarEnvPrefix) {
|
|
continue
|
|
}
|
|
raw = raw[len(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.ValueFromAutoFile, ret)
|
|
diags = diags.Append(moreDiags)
|
|
}
|
|
const defaultVarsFilenameJSON = DefaultVarsFilename + ".json"
|
|
if _, err := os.Stat(defaultVarsFilenameJSON); err == nil {
|
|
moreDiags := m.addVarsFromFile(defaultVarsFilenameJSON, terraform.ValueFromAutoFile, 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.ValueFromAutoFile, 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.ValueFromNamedFile, 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
|
|
}
|
|
}
|
|
|
|
// Before we do our real decode, we'll probe to see if there are any blocks
|
|
// of type "variable" in this body, since it's a common mistake for new
|
|
// users to put variable declarations in tfvars rather than variable value
|
|
// definitions, and otherwise our error message for that case is not so
|
|
// helpful.
|
|
{
|
|
content, _, _ := f.Body.PartialContent(&hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: "variable",
|
|
LabelNames: []string{"name"},
|
|
},
|
|
},
|
|
})
|
|
for _, block := range content.Blocks {
|
|
name := block.Labels[0]
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Variable declaration in .tfvars file",
|
|
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),
|
|
Subject: &block.TypeRange,
|
|
})
|
|
}
|
|
if diags.HasErrors() {
|
|
// If we already found problems then JustAttributes below will find
|
|
// the same problems with less-helpful messages, so we'll bail for
|
|
// now to let the user focus on the immediate problem.
|
|
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
|
|
}
|