From e21f0fa61e2f95a5cdbad6a3be5727723b5d7b74 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 8 Oct 2019 12:08:27 -0700 Subject: [PATCH] backend/local: Handle interactive prompts for variables in UI layer During the 0.12 work we intended to move all of the variable value collection logic into the UI layer (command package and backend packages) and present them all together as a unified data structure to Terraform Core. However, we didn't quite succeed because the interactive prompts for unset required variables were still being handled _after_ calling into Terraform Core. Here we complete that earlier work by moving the interactive prompts for variables out into the UI layer too, thus allowing us to handle final validation of the variables all together in one place and do so in the UI layer where we have the most context still available about where all of these values are coming from. This allows us to fix a problem where previously disabling input with -input=false on the command line could cause Terraform Core to receive an incomplete set of variable values, and fail with a bad error message. As a consequence of this refactoring, the scope of terraform.Context.Input is now reduced to only gathering provider configuration arguments. Ideally that too would move into the UI layer somehow in a future commit, but that's a problem for another day. --- backend/local/backend_local.go | 101 ++++++++- backend/unparsed_value.go | 49 ++++ backend/unparsed_value_test.go | 78 ++++++- command/apply_test.go | 6 +- command/meta.go | 2 - command/meta_test.go | 61 +---- command/plan_test.go | 4 + configs/named_values.go | 6 + terraform/context.go | 11 +- terraform/context_input.go | 94 ++------ terraform/context_input_test.go | 384 -------------------------------- 11 files changed, 251 insertions(+), 545 deletions(-) diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 58bc4f5cb..433c628ea 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "log" + "sort" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/states/statemgr" @@ -103,8 +105,6 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload. // If input asking is enabled, then do that if op.PlanFile == nil && b.OpInput { mode := terraform.InputModeProvider - mode |= terraform.InputModeVar - mode |= terraform.InputModeVarUnset log.Printf("[TRACE] backend/local: requesting interactive input, if necessary") inputDiags := tfCtx.Input(mode) @@ -136,14 +136,18 @@ func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts) } opts.Config = config - variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables) + // 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) + + variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables) diags = diags.Append(varDiags) if diags.HasErrors() { return nil, nil, diags } - if op.Variables != nil { - opts.Variables = variables - } + opts.Variables = variables tfCtx, ctxDiags := terraform.NewContext(&opts) diags = diags.Append(ctxDiags) @@ -245,6 +249,91 @@ func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextO return tfCtx, snap, diags } +// interactiveCollectVariables attempts to complete the given existing +// map of variables by interactively prompting for any variables that are +// declared as required but not yet present. +// +// If interactive input is disabled for this backend instance then this is +// a no-op. If input is enabled but fails for some reason, the resulting +// map will be incomplete. For these reasons, the caller must still validate +// that the result is complete and valid. +// +// This function does not modify the map given in "existing", but may return +// it unchanged if no modifications are required. If modifications are required, +// the result is a new map with all of the elements from "existing" plus +// additional elements as appropriate. +// +// Interactive prompting is a "best effort" thing for first-time user UX and +// not something we expect folks to be relying on for routine use. Terraform +// is primarily a non-interactive tool and so we prefer to report in error +// messages that variables are not set rather than reporting that input failed: +// the primary resolution to missing variables is to provide them by some other +// means. +func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { + var needed []string + if b.OpInput && uiInput != nil { + for name, vc := range vcs { + if !vc.Required() { + continue // We only prompt for required variables + } + if _, exists := existing[name]; !exists { + needed = append(needed, name) + } + } + } else { + log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled") + } + if len(needed) == 0 { + return existing + } + + log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed) + + // If we get here then we're planning to prompt for at least one additional + // variable's value. + sort.Strings(needed) // prompt in lexical order + ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) + for k, v := range existing { + ret[k] = v + } + for _, name := range needed { + vc := vcs[name] + rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ + Id: fmt.Sprintf("var.%s", name), + Query: fmt.Sprintf("var.%s", name), + Description: vc.Description, + }) + if err != nil { + // Since interactive prompts are best-effort, we'll just continue + // here and let subsequent validation report this as a variable + // not specified. + log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err) + continue + } + ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue} + } + return ret +} + +type unparsedInteractiveVariableValue struct { + Name, RawValue string +} + +var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} + +func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + val, valDiags := mode.Parse(v.Name, v.RawValue) + diags = diags.Append(valDiags) + if diags.HasErrors() { + return nil, diags + } + return &terraform.InputValue{ + Value: val, + SourceType: terraform.ValueFromInput, + }, diags +} + 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 diff --git a/backend/unparsed_value.go b/backend/unparsed_value.go index 1b60161f0..a1f0468a0 100644 --- a/backend/unparsed_value.go +++ b/backend/unparsed_value.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" ) // UnparsedVariableValue represents a variable value provided by the caller @@ -24,6 +25,19 @@ type UnparsedVariableValue interface { ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) } +// ParseVariableValues processes a map of unparsed variable values by +// correlating each one with the given variable declarations which should +// be from a root module. +// +// The map of unparsed variable values should include variables from all +// possible root module declarations sources such that it is as complete as +// it can possibly be for the current operation. If any declared variables +// are not included in the map, ParseVariableValues will either substitute +// a configured default value or produce an error. +// +// If this function returns without any errors in the diagnostics, the +// resulting input values map is guaranteed to be valid and ready to pass +// to terraform.NewContext. func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics ret := make(terraform.InputValues, len(vv)) @@ -107,5 +121,40 @@ func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]* }) } + // By this point we should've gathered all of the required root module + // variables from one of the many possible sources. We'll now populate + // any we haven't gathered as their defaults and fail if any of the + // missing ones are required. + for name, vc := range decls { + if _, defined := ret[name]; defined { + continue + } + + if vc.Required() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No value for required variable", + Detail: fmt.Sprintf("The root module input variable %q is not set, and has no default value. Use a -var or -var-file command line argument to provide a value for this variable.", name), + Subject: vc.DeclRange.Ptr(), + }) + + // We'll include a placeholder value anyway, just so that our + // result is complete for any calling code that wants to cautiously + // analyze it for diagnostic purposes. Since our diagnostics now + // includes an error, normal processing will ignore this result. + ret[name] = &terraform.InputValue{ + Value: cty.DynamicVal, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), + } + } else { + ret[name] = &terraform.InputValue{ + Value: vc.Default, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange), + } + } + } + return ret, diags } diff --git a/backend/unparsed_value_test.go b/backend/unparsed_value_test.go index 6112d7c71..27fba6257 100644 --- a/backend/unparsed_value_test.go +++ b/backend/unparsed_value_test.go @@ -3,6 +3,8 @@ package backend import ( "testing" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/configs" @@ -17,19 +19,53 @@ func TestParseVariableValuesUndeclared(t *testing.T) { "undeclared2": testUnparsedVariableValue("2"), "undeclared3": testUnparsedVariableValue("3"), "undeclared4": testUnparsedVariableValue("4"), + "declared1": testUnparsedVariableValue("5"), + } + decls := map[string]*configs.Variable{ + "declared1": { + Name: "declared1", + Type: cty.String, + ParsingMode: configs.VariableParseLiteral, + DeclRange: hcl.Range{ + Filename: "fake.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 2, Column: 1, Byte: 0}, + }, + }, + "missing1": { + Name: "missing1", + Type: cty.String, + ParsingMode: configs.VariableParseLiteral, + DeclRange: hcl.Range{ + Filename: "fake.tf", + Start: hcl.Pos{Line: 3, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 0}, + }, + }, + "missing2": { + Name: "missing1", + Type: cty.String, + ParsingMode: configs.VariableParseLiteral, + Default: cty.StringVal("default for missing2"), + DeclRange: hcl.Range{ + Filename: "fake.tf", + Start: hcl.Pos{Line: 4, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 4, Column: 1, Byte: 0}, + }, + }, } - decls := map[string]*configs.Variable{} - _, diags := ParseVariableValues(vv, decls) + gotVals, diags := ParseVariableValues(vv, decls) for _, diag := range diags { t.Logf("%s: %s", diag.Description().Summary, diag.Description().Detail) } - if got, want := len(diags), 4; got != want { + if got, want := len(diags), 5; got != want { t.Fatalf("wrong number of diagnostics %d; want %d", got, want) } const undeclSingular = `Value for undeclared variable` const undeclPlural = `Values for undeclared variables` + const missingRequired = `No value for required variable` if got, want := diags[0].Description().Summary, undeclSingular; got != want { t.Errorf("wrong summary for diagnostic 0\ngot: %s\nwant: %s", got, want) @@ -43,6 +79,42 @@ func TestParseVariableValuesUndeclared(t *testing.T) { if got, want := diags[3].Description().Summary, undeclPlural; got != want { t.Errorf("wrong summary for diagnostic 3\ngot: %s\nwant: %s", got, want) } + if got, want := diags[4].Description().Summary, missingRequired; got != want { + t.Errorf("wrong summary for diagnostic 4\ngot: %s\nwant: %s", got, want) + } + + wantVals := terraform.InputValues{ + "declared1": { + Value: cty.StringVal("5"), + SourceType: terraform.ValueFromNamedFile, + SourceRange: tfdiags.SourceRange{ + Filename: "fake.tfvars", + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + }, + }, + "missing1": { + Value: cty.DynamicVal, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRange{ + Filename: "fake.tf", + Start: tfdiags.SourcePos{Line: 3, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 3, Column: 1, Byte: 0}, + }, + }, + "missing2": { + Value: cty.StringVal("default for missing2"), + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRange{ + Filename: "fake.tf", + Start: tfdiags.SourcePos{Line: 4, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 4, Column: 1, Byte: 0}, + }, + }, + } + if diff := cmp.Diff(wantVals, gotVals, cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("wrong result\n%s", diff) + } } type testUnparsedVariableValue string diff --git a/command/apply_test.go b/command/apply_test.go index b949b6af7..bfc396438 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -423,7 +423,11 @@ func TestApply_input(t *testing.T) { test = false defer func() { test = true }() - // Set some default reader/writers for the inputs + // The configuration for this test includes a declaration of variable + // "foo" with no default, and we don't set it on the command line below, + // so the apply command will produce an interactive prompt for the + // value of var.foo. We'll answer "foo" here, and we expect the output + // value "result" to echo that back to us below. defaultInputReader = bytes.NewBufferString("foo\n") defaultInputWriter = new(bytes.Buffer) diff --git a/command/meta.go b/command/meta.go index d55c9f8a5..6399c8c45 100644 --- a/command/meta.go +++ b/command/meta.go @@ -242,8 +242,6 @@ func (m *Meta) InputMode() terraform.InputMode { var mode terraform.InputMode mode |= terraform.InputModeProvider - mode |= terraform.InputModeVar - mode |= terraform.InputModeVarUnset return mode } diff --git a/command/meta_test.go b/command/meta_test.go index 978dca8c7..10ea80406 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -78,7 +78,7 @@ func TestMetaInputMode(t *testing.T) { t.Fatalf("err: %s", err) } - if m.InputMode() != terraform.InputModeStd|terraform.InputModeVarUnset { + if m.InputMode() != terraform.InputModeStd { t.Fatalf("bad: %#v", m.InputMode()) } } @@ -98,7 +98,7 @@ func TestMetaInputMode_envVar(t *testing.T) { } off := terraform.InputMode(0) - on := terraform.InputModeStd | terraform.InputModeVarUnset + on := terraform.InputModeStd cases := []struct { EnvVar string Expected terraform.InputMode @@ -134,63 +134,6 @@ func TestMetaInputMode_disable(t *testing.T) { } } -func TestMetaInputMode_defaultVars(t *testing.T) { - test = false - defer func() { test = true }() - - // Create a temporary directory for our cwd - d := tempDir(t) - os.MkdirAll(d, 0755) - defer os.RemoveAll(d) - defer testChdir(t, d)() - - // Create the default vars file - err := ioutil.WriteFile( - filepath.Join(d, DefaultVarsFilename), - []byte(""), - 0644) - if err != nil { - t.Fatalf("err: %s", err) - } - - m := new(Meta) - args := []string{} - args, err = m.process(args, false) - if err != nil { - t.Fatalf("err: %s", err) - } - - fs := m.extendedFlagSet("foo") - if err := fs.Parse(args); err != nil { - t.Fatalf("err: %s", err) - } - - if m.InputMode()&terraform.InputModeVar == 0 { - t.Fatalf("bad: %#v", m.InputMode()) - } -} - -func TestMetaInputMode_vars(t *testing.T) { - test = false - defer func() { test = true }() - - m := new(Meta) - args := []string{"-var", "foo=bar"} - - fs := m.extendedFlagSet("foo") - if err := fs.Parse(args); err != nil { - t.Fatalf("err: %s", err) - } - - if m.InputMode()&terraform.InputModeVar == 0 { - t.Fatalf("bad: %#v", m.InputMode()) - } - - if m.InputMode()&terraform.InputModeVarUnset == 0 { - t.Fatalf("bad: %#v", m.InputMode()) - } -} - func TestMeta_initStatePaths(t *testing.T) { m := new(Meta) m.initStatePaths() diff --git a/command/plan_test.go b/command/plan_test.go index 2d384edf4..6885810d5 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -575,6 +575,10 @@ func TestPlan_varsUnset(t *testing.T) { test = false defer func() { test = true }() + // The plan command will prompt for interactive input of var.foo. + // We'll answer "bar" to that prompt, which should then allow this + // configuration to apply even though var.foo doesn't have a + // default value and there are no -var arguments on our command line. defaultInputReader = bytes.NewBufferString("bar\n") p := planVarsFixtureProvider() diff --git a/configs/named_values.go b/configs/named_values.go index 81f6093e3..fa51c76ae 100644 --- a/configs/named_values.go +++ b/configs/named_values.go @@ -179,6 +179,12 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl } } +// Required returns true if this variable is required to be set by the caller, +// or false if there is a default value that will be used when it isn't set. +func (v *Variable) Required() bool { + return v.Default == cty.NilVal +} + // VariableParsingMode defines how values of a particular variable given by // text-only mechanisms (command line arguments and environment variables) // should be parsed to produce the final value. diff --git a/terraform/context.go b/terraform/context.go index b0162f6e4..911ad088d 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -24,19 +24,12 @@ import ( type InputMode byte const ( - // InputModeVar asks for all variables - InputModeVar InputMode = 1 << iota - - // InputModeVarUnset asks for variables which are not set yet. - // InputModeVar must be set for this to have an effect. - InputModeVarUnset - // InputModeProvider asks for provider variables - InputModeProvider + InputModeProvider InputMode = 1 << iota // InputModeStd is the standard operating mode and asks for both variables // and providers. - InputModeStd = InputModeVar | InputModeProvider + InputModeStd = InputModeProvider ) var ( diff --git a/terraform/context_input.go b/terraform/context_input.go index 4ad227143..d24adcb7c 100644 --- a/terraform/context_input.go +++ b/terraform/context_input.go @@ -2,7 +2,6 @@ package terraform import ( "context" - "fmt" "log" "sort" @@ -15,10 +14,22 @@ import ( "github.com/hashicorp/terraform/tfdiags" ) -// Input asks for input to fill variables and provider configurations. +// Input asks for input to fill unset required arguments in provider +// configurations. +// // This modifies the configuration in-place, so asking for Input twice // may result in different UI output showing different current values. func (c *Context) Input(mode InputMode) tfdiags.Diagnostics { + // This function used to be responsible for more than it is now, so its + // interface is more general than its current functionality requires. + // It now exists only to handle interactive prompts for provider + // configurations, with other prompts the responsibility of the CLI + // layer prior to calling in to this package. + // + // (Hopefully in future the remaining functionality here can move to the + // CLI layer too in order to avoid this odd situation where core code + // produces UI input prompts.) + var diags tfdiags.Diagnostics defer c.acquireRun("input")() @@ -29,85 +40,6 @@ func (c *Context) Input(mode InputMode) tfdiags.Diagnostics { ctx := context.Background() - if mode&InputModeVar != 0 { - log.Printf("[TRACE] Context.Input: Prompting for variables") - - // Walk the variables first for the root module. We walk them in - // alphabetical order for UX reasons. - configs := c.config.Module.Variables - names := make([]string, 0, len(configs)) - for name := range configs { - names = append(names, name) - } - sort.Strings(names) - Variables: - for _, n := range names { - v := configs[n] - - // If we only care about unset variables, then we should set any - // variable that is already set. - if mode&InputModeVarUnset != 0 { - if _, isSet := c.variables[n]; isSet { - continue - } - } - - // this should only happen during tests - if c.uiInput == nil { - log.Println("[WARN] Context.uiInput is nil during input walk") - continue - } - - // Ask the user for a value for this variable - var rawValue string - retry := 0 - for { - var err error - rawValue, err = c.uiInput.Input(ctx, &InputOpts{ - Id: fmt.Sprintf("var.%s", n), - Query: fmt.Sprintf("var.%s", n), - Description: v.Description, - }) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to request interactive input", - fmt.Sprintf("Terraform attempted to request a value for var.%s interactively, but encountered an error: %s.", n, err), - )) - return diags - } - - if rawValue == "" && v.Default == cty.NilVal { - // Redo if it is required, but abort if we keep getting - // blank entries - if retry > 2 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Required variable not assigned", - fmt.Sprintf("The variable %q is required, so Terraform cannot proceed without a defined value for it.", n), - )) - continue Variables - } - retry++ - continue - } - - break - } - - val, valDiags := v.ParsingMode.Parse(n, rawValue) - diags = diags.Append(valDiags) - if diags.HasErrors() { - continue - } - - c.variables[n] = &InputValue{ - Value: val, - SourceType: ValueFromInput, - } - } - } - if mode&InputModeProvider != 0 { log.Printf("[TRACE] Context.Input: Prompting for provider arguments") diff --git a/terraform/context_input_test.go b/terraform/context_input_test.go index f6b5a4f36..312ca1d7c 100644 --- a/terraform/context_input_test.go +++ b/terraform/context_input_test.go @@ -14,92 +14,6 @@ import ( "github.com/hashicorp/terraform/states" ) -func TestContext2Input(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-vars") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{ - "amis": &InputValue{ - Value: cty.MapVal(map[string]cty.Value{ - "us-east-1": cty.StringVal("override"), - }), - SourceType: ValueFromCaller, - }, - }, - UIInput: input, - }) - - input.InputReturnMap = map[string]string{ - "var.foo": "us-east-1", - } - - if diags := ctx.Input(InputModeStd | InputModeVarUnset); diags.HasErrors() { - t.Fatalf("input errors: %s", diags.Err()) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } - - state, diags := ctx.Apply() - if diags.HasErrors() { - t.Fatalf("apply errors: %s", diags.Err()) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformInputVarsStr) - if actual != expected { - t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual) - } -} - -func TestContext2Input_moduleComputedOutputElement(t *testing.T) { - m := testModule(t, "input-module-computed-output-element") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) - - if diags := ctx.Input(InputModeStd); diags.HasErrors() { - t.Fatalf("input errors: %s", diags.Err()) - } -} - -func TestContext2Input_badVarDefault(t *testing.T) { - m := testModule(t, "input-bad-var-default") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - }) - - if diags := ctx.Input(InputModeStd); diags.HasErrors() { - t.Fatalf("input errors: %s", diags.Err()) - } -} - func TestContext2Input_provider(t *testing.T) { m := testModule(t, "input-provider") p := testProvider("aws") @@ -497,304 +411,6 @@ func TestContext2Input_providerVarsModuleInherit(t *testing.T) { } } -func TestContext2Input_varOnly(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-provider-vars") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{ - "foo": &InputValue{ - Value: cty.StringVal("us-west-2"), - SourceType: ValueFromCaller, - }, - }, - UIInput: input, - }) - - input.InputReturnMap = map[string]string{ - "var.foo": "us-east-1", - } - - var actual interface{} - /*p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) { - c.Raw["foo"] = "bar" - return c, nil - }*/ - p.ConfigureFn = func(c *ResourceConfig) error { - actual = c.Raw["foo"] - return nil - } - - if err := ctx.Input(InputModeVar); err != nil { - t.Fatalf("err: %s", err) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } - - state, err := ctx.Apply() - if err != nil { - t.Fatalf("err: %s", err) - } - - if reflect.DeepEqual(actual, "bar") { - t.Fatalf("bad: %#v", actual) - } - - actualStr := strings.TrimSpace(state.String()) - expectedStr := strings.TrimSpace(testTerraformInputVarOnlyStr) - if actualStr != expectedStr { - t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actualStr, expectedStr) - } -} - -func TestContext2Input_varOnlyUnset(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-vars-unset") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{ - "foo": &InputValue{ - Value: cty.StringVal("foovalue"), - SourceType: ValueFromCaller, - }, - }, - UIInput: input, - }) - - input.InputReturnMap = map[string]string{ - "var.foo": "nope", - "var.bar": "baz", - } - - if err := ctx.Input(InputModeVar | InputModeVarUnset); err != nil { - t.Fatalf("err: %s", err) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } - - state, err := ctx.Apply() - if err != nil { - t.Fatalf("err: %s", err) - } - - actualStr := strings.TrimSpace(state.String()) - expectedStr := strings.TrimSpace(testTerraformInputVarOnlyUnsetStr) - if actualStr != expectedStr { - t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actualStr, expectedStr) - } -} - -func TestContext2Input_varWithDefault(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-var-default") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{}, - UIInput: input, - }) - - input.InputFn = func(opts *InputOpts) (string, error) { - t.Fatalf( - "Input should never be called because variable has a default: %#v", opts) - return "", nil - } - - if err := ctx.Input(InputModeVar | InputModeVarUnset); err != nil { - t.Fatalf("err: %s", err) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } - - state, err := ctx.Apply() - if err != nil { - t.Fatalf("err: %s", err) - } - - actualStr := strings.TrimSpace(state.String()) - expectedStr := strings.TrimSpace(` -aws_instance.foo: - ID = foo - provider = provider.aws - foo = 123 - type = aws_instance - `) - if actualStr != expectedStr { - t.Fatalf("expected: \n%s\ngot: \n%s\n", expectedStr, actualStr) - } -} - -func TestContext2Input_varPartiallyComputed(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-var-partially-computed") - p := testProvider("aws") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "aws": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{ - "foo": &InputValue{ - Value: cty.StringVal("foovalue"), - SourceType: ValueFromCaller, - }, - }, - UIInput: input, - State: states.BuildState(func(s *states.SyncState) { - s.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - &states.ResourceInstanceObjectSrc{ - AttrsFlat: map[string]string{ - "id": "i-abc123", - }, - Status: states.ObjectReady, - }, - addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), - ) - s.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "aws_instance", - Name: "mode", - }.Instance(addrs.NoKey).Absolute(addrs.Module{"child"}.UnkeyedInstanceShim()), - &states.ResourceInstanceObjectSrc{ - AttrsFlat: map[string]string{ - "id": "i-bcd345", - "value": "one,i-abc123", - }, - Status: states.ObjectReady, - }, - addrs.ProviderConfig{Type: "aws"}.Absolute(addrs.RootModuleInstance), - ) - }), - }) - - if diags := ctx.Input(InputModeStd); diags.HasErrors() { - t.Fatalf("input errors: %s", diags.Err()) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } -} - -// Module variables weren't being interpolated during the Input walk. -// https://github.com/hashicorp/terraform/issues/5322 -func TestContext2Input_interpolateVar(t *testing.T) { - input := new(MockUIInput) - - m := testModule(t, "input-interpolate-var") - p := testProvider("null") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "template": testProviderFuncFixed(p), - }, - ), - UIInput: input, - }) - - if diags := ctx.Input(InputModeStd); diags.HasErrors() { - t.Fatalf("input errors: %s", diags.Err()) - } -} - -func TestContext2Input_hcl(t *testing.T) { - input := new(MockUIInput) - m := testModule(t, "input-hcl") - p := testProvider("hcl") - p.ApplyFn = testApplyFn - p.DiffFn = testDiffFn - p.GetSchemaReturn = &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "hcl_instance": { - Attributes: map[string]*configschema.Attribute{ - "foo": {Type: cty.List(cty.String), Optional: true}, - "bar": {Type: cty.Map(cty.String), Optional: true}, - "id": {Type: cty.String, Computed: true}, - "type": {Type: cty.String, Computed: true}, - }, - }, - }, - } - ctx := testContext2(t, &ContextOpts{ - Config: m, - ProviderResolver: providers.ResolverFixed( - map[string]providers.Factory{ - "hcl": testProviderFuncFixed(p), - }, - ), - Variables: InputValues{}, - UIInput: input, - }) - - input.InputReturnMap = map[string]string{ - "var.listed": `["a", "b"]`, - "var.mapped": `{x = "y", w = "z"}`, - } - - if err := ctx.Input(InputModeVar | InputModeVarUnset); err != nil { - t.Fatalf("err: %s", err) - } - - if _, diags := ctx.Plan(); diags.HasErrors() { - t.Fatalf("plan errors: %s", diags.Err()) - } - - state, err := ctx.Apply() - if err != nil { - t.Fatalf("err: %s", err) - } - - actualStr := strings.TrimSpace(state.String()) - expectedStr := strings.TrimSpace(testTerraformInputHCL) - if actualStr != expectedStr { - t.Logf("expected: \n%s", expectedStr) - t.Fatalf("bad: \n%s", actualStr) - } -} - // adding a list interpolation in fails to interpolate the count variable func TestContext2Input_submoduleTriggersInvalidCount(t *testing.T) { input := new(MockUIInput)