terraform: Input() asks for variable inputs
This commit is contained in:
parent
2f681c4bcc
commit
fd70e5e7bf
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue