terraform: Input() asks for variable inputs

This commit is contained in:
Mitchell Hashimoto 2014-09-28 23:37:36 -07:00
parent 2f681c4bcc
commit fd70e5e7bf
6 changed files with 266 additions and 2 deletions

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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)}"
}

18
terraform/ui_input.go Normal file
View File

@ -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
}

View File

@ -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
}