From fd70e5e7bf26dea11c16192dd4caf7959ab08b78 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Sep 2014 23:37:36 -0700 Subject: [PATCH] terraform: Input() asks for variable inputs --- terraform/context.go | 150 ++++++++++++++++++++- terraform/context_test.go | 42 ++++++ terraform/terraform_test.go | 13 ++ terraform/test-fixtures/input-vars/main.tf | 22 +++ terraform/ui_input.go | 18 +++ terraform/ui_input_mock.go | 23 ++++ 6 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 terraform/test-fixtures/input-vars/main.tf create mode 100644 terraform/ui_input.go create mode 100644 terraform/ui_input_mock.go diff --git a/terraform/context.go b/terraform/context.go index 28dd0d5f9..afeebfc68 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -3,6 +3,7 @@ package terraform import ( "fmt" "log" + "sort" "strconv" "strings" "sync" @@ -31,6 +32,7 @@ type Context struct { providers map[string]ResourceProviderFactory provisioners map[string]ResourceProvisionerFactory variables map[string]string + uiInput UIInput l sync.Mutex // Lock acquired during any task parCh chan struct{} // Semaphore used to limit parallelism @@ -50,6 +52,8 @@ type ContextOpts struct { Providers map[string]ResourceProviderFactory Provisioners map[string]ResourceProvisionerFactory Variables map[string]string + + UIInput UIInput } // NewContext creates a new context. @@ -81,6 +85,7 @@ func NewContext(opts *ContextOpts) *Context { providers: opts.Providers, provisioners: opts.Provisioners, variables: opts.Variables, + uiInput: opts.UIInput, parCh: parCh, sh: sh, @@ -126,6 +131,74 @@ func (c *Context) Graph() (*depgraph.Graph, error) { }) } +// Input asks for input to fill variables and 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() error { + v := c.acquireRun() + defer c.releaseRun(v) + + // Walk the variables first for the root module. We walk them in + // alphabetical order for UX reasons. + rootConf := c.module.Config() + names := make([]string, len(rootConf.Variables)) + m := make(map[string]*config.Variable) + for i, v := range rootConf.Variables { + names[i] = v.Name + m[v.Name] = v + } + sort.Strings(names) + for _, n := range names { + v := m[n] + switch v.Type() { + case config.VariableTypeMap: + continue + case config.VariableTypeString: + // Good! + default: + panic(fmt.Sprintf("Unknown variable type: %s", v.Type())) + } + + // Ask the user for a value for this variable + var value string + for { + var err error + value, err = c.uiInput.Input(&InputOpts{ + Id: fmt.Sprintf("var.%s", n), + Query: fmt.Sprintf( + "Please enter a value for '%s': ", n), + }) + if err != nil { + return fmt.Errorf( + "Error asking for %s: %s", n, err) + } + + if value == "" && v.Required() { + // Redo if it is required. + continue + } + + if value == "" { + // No value, just exit the loop. With no value, we just + // use whatever is currently set in variables. + break + } + + break + } + + if value != "" { + c.variables[n] = value + } + } + + // Create the walk context and walk the inputs, which will gather the + // inputs for any resource providers. + wc := c.walkContext(walkInput, rootModulePath) + wc.Meta = new(walkInputMeta) + return wc.Walk() +} + // Plan generates an execution plan for the given context. // // The execution plan encapsulates the context and can be stored @@ -337,6 +410,7 @@ type walkOperation byte const ( walkInvalid walkOperation = iota + walkInput walkApply walkPlan walkPlanDestroy @@ -366,6 +440,8 @@ func (c *walkContext) Walk() error { var walkFn depgraph.WalkFunc switch c.Operation { + case walkInput: + walkFn = c.inputWalkFn() case walkApply: walkFn = c.applyWalkFn() case walkPlan: @@ -384,8 +460,11 @@ func (c *walkContext) Walk() error { return err } - if c.Operation == walkValidate { - // Validation is the only one that doesn't calculate outputs + switch c.Operation { + case walkInput: + fallthrough + case walkValidate: + // Don't calculate outputs return nil } @@ -439,6 +518,67 @@ func (c *walkContext) Walk() error { return nil } +func (c *walkContext) inputWalkFn() depgraph.WalkFunc { + meta := c.Meta.(*walkInputMeta) + meta.Lock() + if meta.Done == nil { + meta.Done = make(map[string]struct{}) + } + meta.Unlock() + + return func(n *depgraph.Noun) error { + // If it is the root node, ignore + if n.Name == GraphRootNode { + return nil + } + + switch rn := n.Meta.(type) { + case *GraphNodeModule: + // Build another walkContext for this module and walk it. + wc := c.Context.walkContext(c.Operation, rn.Path) + + // Set the graph to specifically walk this subgraph + wc.graph = rn.Graph + + // Preserve the meta + wc.Meta = c.Meta + + return wc.Walk() + case *GraphNodeResource: + // Resources don't matter for input. Continue. + return nil + case *GraphNodeResourceProvider: + return nil + /* + // If we already did this provider, then we're done. + meta.Lock() + _, ok := meta.Done[rn.ID] + meta.Unlock() + if ok { + return nil + } + + // Get the raw configuration because this is what we + // pass into the API. + var raw *config.RawConfig + sharedProvider := rn.Provider + if sharedProvider.Config != nil { + raw = sharedProvider.Config.RawConfig + } + rc := NewResourceConfig(raw) + + // Go through each provider and capture the input necessary + // to satisfy it. + for k, p := range sharedProvider.Providers { + ws, es := p.Validate(rc) + } + */ + } + + return nil + } +} + func (c *walkContext) applyWalkFn() depgraph.WalkFunc { cb := func(c *walkContext, r *Resource) error { var err error @@ -1385,6 +1525,12 @@ func (c *walkContext) computeResourceMultiVariable( return strings.Join(values, ","), nil } +type walkInputMeta struct { + sync.Mutex + + Done map[string]struct{} +} + type walkValidateMeta struct { Errs []error Warns []string diff --git a/terraform/context_test.go b/terraform/context_test.go index 67144b2df..45064de3e 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -418,6 +418,48 @@ func TestContextValidate_selfRefMultiAll(t *testing.T) { } } +func TestContextInput(t *testing.T) { + input := new(MockUIInput) + m := testModule(t, "input-vars") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "foo": "us-west-2", + "amis.us-east-1": "override", + }, + UIInput: input, + }) + + input.InputReturnMap = map[string]string{ + "var.foo": "us-east-1", + } + + if err := ctx.Input(); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformInputVarsStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + func TestContextApply(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index a6fbea9de..6be8cdd4b 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -113,6 +113,19 @@ func (h *HookRecordApplyOrder) PreApply( // Below are all the constant strings that are the expected output for // various tests. +const testTerraformInputVarsStr = ` +aws_instance.bar: + ID = foo + bar = override + foo = us-east-1 + type = aws_instance +aws_instance.foo: + ID = foo + bar = baz + num = 2 + type = aws_instance +` + const testTerraformApplyStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/input-vars/main.tf b/terraform/test-fixtures/input-vars/main.tf new file mode 100644 index 000000000..9c2d0b67d --- /dev/null +++ b/terraform/test-fixtures/input-vars/main.tf @@ -0,0 +1,22 @@ +variable "amis" { + default = { + us-east-1 = "foo" + us-west-2 = "bar" + } +} + +variable "bar" { + default = "baz" +} + +variable "foo" {} + +resource "aws_instance" "foo" { + num = "2" + bar = "${var.bar}" +} + +resource "aws_instance" "bar" { + foo = "${var.foo}" + bar = "${lookup(var.amis, var.foo)}" +} diff --git a/terraform/ui_input.go b/terraform/ui_input.go new file mode 100644 index 000000000..46f6378d1 --- /dev/null +++ b/terraform/ui_input.go @@ -0,0 +1,18 @@ +package terraform + +// UIInput is the interface that must be implemented to ask for input +// from this user. This should forward the request to wherever the user +// inputs things to ask for values. +type UIInput interface { + Input(*InputOpts) (string, error) +} + +// InputOpts are options for asking for input. +type InputOpts struct { + // Id is a unique ID for the question being asked that might be + // used for logging or to look up a prior answered question. + Id string + + // Query is a human-friendly question for inputting this value. + Query string +} diff --git a/terraform/ui_input_mock.go b/terraform/ui_input_mock.go new file mode 100644 index 000000000..e3a07efa3 --- /dev/null +++ b/terraform/ui_input_mock.go @@ -0,0 +1,23 @@ +package terraform + +// MockUIInput is an implementation of UIInput that can be used for tests. +type MockUIInput struct { + InputCalled bool + InputOpts *InputOpts + InputReturnMap map[string]string + InputReturnString string + InputReturnError error + InputFn func(*InputOpts) (string, error) +} + +func (i *MockUIInput) Input(opts *InputOpts) (string, error) { + i.InputCalled = true + i.InputOpts = opts + if i.InputFn != nil { + return i.InputFn(opts) + } + if i.InputReturnMap != nil { + return i.InputReturnMap[opts.Id], i.InputReturnError + } + return i.InputReturnString, i.InputReturnError +}