backend: Allow certain commands to opt out of required variable checks
Terraform Core expects all variables to be set, but for some ancillary commands it's fine for them to just be set to placeholders because the variable values themselves are not key to the command's functionality as long as the terraform.Context is still self-consistent. For such commands, rather than prompting for interactive input for required variables we'll just stub them out as unknowns to reflect that they are placeholders for values that a user would normally need to provide. This achieves a similar effect to how these commands behaved before, but without the tendency to produce a slightly invalid terraform.Context that would fail in strange ways when asked to run certain operations.
This commit is contained in:
parent
e21f0fa61e
commit
8664749b59
|
@ -196,6 +196,16 @@ type Operation struct {
|
|||
Targets []addrs.Targetable
|
||||
Variables map[string]UnparsedVariableValue
|
||||
|
||||
// Some operations use root module variables only opportunistically or
|
||||
// don't need them at all. If this flag is set, the backend must treat
|
||||
// all variables as optional and provide an unknown value for any required
|
||||
// variables that aren't set in order to allow partial evaluation against
|
||||
// the resulting incomplete context.
|
||||
//
|
||||
// This flag is honored only if PlanFile isn't set. If PlanFile is set then
|
||||
// the variables set in the plan are used instead, and they must be valid.
|
||||
AllowUnsetVariables bool
|
||||
|
||||
// Input/output/control options.
|
||||
UIIn terraform.UIInput
|
||||
UIOut terraform.UIOutput
|
||||
|
|
|
@ -136,11 +136,20 @@ func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts)
|
|||
}
|
||||
opts.Config = config
|
||||
|
||||
var rawVariables map[string]backend.UnparsedVariableValue
|
||||
if op.AllowUnsetVariables {
|
||||
// Rather than prompting for input, we'll just stub out the required
|
||||
// but unset variables with unknown values to represent that they are
|
||||
// placeholders for values the user would need to provide for other
|
||||
// operations.
|
||||
rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables)
|
||||
} else {
|
||||
// If interactive input is enabled, we might gather some more variable
|
||||
// values through interactive prompts.
|
||||
// TODO: Need to route the operation context through into here, so that
|
||||
// the interactive prompts can be sensitive to its timeouts/etc.
|
||||
rawVariables := b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, opts.UIInput)
|
||||
rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, opts.UIInput)
|
||||
}
|
||||
|
||||
variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables)
|
||||
diags = diags.Append(varDiags)
|
||||
|
@ -315,6 +324,60 @@ func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[st
|
|||
return ret
|
||||
}
|
||||
|
||||
// stubUnsetVariables ensures that all required variables defined in the
|
||||
// configuration exist in the resulting map, by adding new elements as necessary.
|
||||
//
|
||||
// The stubbed value of any additions will be an unknown variable conforming
|
||||
// to the variable's configured type constraint, meaning that no particular
|
||||
// value is known and that one must be provided by the user in order to get
|
||||
// a complete result.
|
||||
//
|
||||
// Unset optional attributes (those with default values) will not be populated
|
||||
// by this function, under the assumption that a later step will handle those.
|
||||
// In this sense, stubUnsetRequiredVariables is essentially a non-interactive,
|
||||
// non-error-producing variant of interactiveCollectVariables that creates
|
||||
// placeholders for values the user would be prompted for interactively on
|
||||
// other operations.
|
||||
//
|
||||
// This function should be used only in situations where variables values
|
||||
// will not be directly used and the variables map is being constructed only
|
||||
// to produce a complete Terraform context for some ancillary functionality
|
||||
// like "terraform console", "terraform state ...", etc.
|
||||
//
|
||||
// This function is guaranteed not to modify the given map, but it may return
|
||||
// the given map unchanged if no additions are required. If additions are
|
||||
// required then the result will be a new map containing everything in the
|
||||
// given map plus additional elements.
|
||||
func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue {
|
||||
var missing bool // Do we need to add anything?
|
||||
for name, vc := range vcs {
|
||||
if !vc.Required() {
|
||||
continue // We only stub required variables
|
||||
}
|
||||
if _, exists := existing[name]; !exists {
|
||||
missing = true
|
||||
}
|
||||
}
|
||||
if !missing {
|
||||
return existing
|
||||
}
|
||||
|
||||
// If we get down here then there's at least one variable value to add.
|
||||
ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
|
||||
for k, v := range existing {
|
||||
ret[k] = v
|
||||
}
|
||||
for name, vc := range vcs {
|
||||
if !vc.Required() {
|
||||
continue
|
||||
}
|
||||
if _, exists := existing[name]; !exists {
|
||||
ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type unparsedInteractiveVariableValue struct {
|
||||
Name, RawValue string
|
||||
}
|
||||
|
@ -334,6 +397,20 @@ func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.Variab
|
|||
}, diags
|
||||
}
|
||||
|
||||
type unparsedUnknownVariableValue struct {
|
||||
Name string
|
||||
WantType cty.Type
|
||||
}
|
||||
|
||||
var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{}
|
||||
|
||||
func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
|
||||
return &terraform.InputValue{
|
||||
Value: cty.UnknownVal(v.WantType),
|
||||
SourceType: terraform.ValueFromInput,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const validateWarnHeader = `
|
||||
There are warnings related to your configuration. If no errors occurred,
|
||||
Terraform will continue despite these warnings. It is a good idea to resolve
|
||||
|
|
|
@ -77,6 +77,7 @@ func (c *ConsoleCommand) Run(args []string) int {
|
|||
opReq := c.Operation(b)
|
||||
opReq.ConfigDir = configPath
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
opReq.AllowUnsetVariables = true // we'll just evaluate them as unknown
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
c.showDiagnostics(diags)
|
||||
|
|
|
@ -84,3 +84,52 @@ func TestConsole_tfvars(t *testing.T) {
|
|||
t.Fatalf("bad: %q", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsole_unsetRequiredVars(t *testing.T) {
|
||||
// This test is verifying that it's possible to run "terraform console"
|
||||
// without providing values for all required variables, without
|
||||
// "terraform console" producing an interactive prompt for those variables
|
||||
// or producing errors. Instead, it should allow evaluation in that
|
||||
// partial context but see the unset variables values as being unknown.
|
||||
|
||||
tmp, cwd := testCwd(t)
|
||||
defer testFixCwd(t, tmp, cwd)
|
||||
|
||||
p := testProvider()
|
||||
ui := new(cli.MockUi)
|
||||
c := &ConsoleCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
defer testStdinPipe(t, strings.NewReader("var.foo\n"))()
|
||||
outCloser := testStdoutCapture(t, &output)
|
||||
|
||||
args := []string{
|
||||
// This test fixture includes variable "foo" {}, which we are
|
||||
// intentionally not setting here.
|
||||
testFixturePath("apply-vars"),
|
||||
}
|
||||
code := c.Run(args)
|
||||
outCloser()
|
||||
|
||||
// Because we're running "terraform console" in piped input mode, we're
|
||||
// expecting it to return a nonzero exit status here but the message
|
||||
// must be the one indicating that it did attempt to evaluate var.foo and
|
||||
// got an unknown value in return, rather than an error about var.foo
|
||||
// not being set or a failure to prompt for it.
|
||||
if code == 0 {
|
||||
t.Fatalf("unexpected success\n%s", ui.OutputWriter.String())
|
||||
}
|
||||
|
||||
// The error message should be the one console produces when it encounters
|
||||
// an unknown value.
|
||||
got := ui.ErrorWriter.String()
|
||||
want := `Error: Result depends on values that cannot be determined`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("wrong output\ngot:\n%s\n\nwant string containing %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,7 @@ func (c *GraphCommand) Run(args []string) int {
|
|||
opReq.ConfigDir = configPath
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
opReq.PlanFile = planFile
|
||||
opReq.AllowUnsetVariables = true
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
c.showDiagnostics(diags)
|
||||
|
|
|
@ -81,6 +81,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int {
|
|||
opReq := c.Operation(b)
|
||||
opReq.ConfigDir = cwd
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
opReq.AllowUnsetVariables = true
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
c.showDiagnostics(diags)
|
||||
|
|
|
@ -91,6 +91,7 @@ func (c *ShowCommand) Run(args []string) int {
|
|||
opReq.ConfigDir = cwd
|
||||
opReq.PlanFile = planFile
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
opReq.AllowUnsetVariables = true
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
c.showDiagnostics(diags)
|
||||
|
|
|
@ -72,6 +72,7 @@ func (c *StateShowCommand) Run(args []string) int {
|
|||
|
||||
// Build the operation (required to get the schemas)
|
||||
opReq := c.Operation(b)
|
||||
opReq.AllowUnsetVariables = true
|
||||
opReq.ConfigDir = cwd
|
||||
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
|
|
Loading…
Reference in New Issue