terraform: Input() asks for variable inputs
This commit is contained in:
parent
2f681c4bcc
commit
fd70e5e7bf
|
@ -3,6 +3,7 @@ package terraform
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -31,6 +32,7 @@ type Context struct {
|
||||||
providers map[string]ResourceProviderFactory
|
providers map[string]ResourceProviderFactory
|
||||||
provisioners map[string]ResourceProvisionerFactory
|
provisioners map[string]ResourceProvisionerFactory
|
||||||
variables map[string]string
|
variables map[string]string
|
||||||
|
uiInput UIInput
|
||||||
|
|
||||||
l sync.Mutex // Lock acquired during any task
|
l sync.Mutex // Lock acquired during any task
|
||||||
parCh chan struct{} // Semaphore used to limit parallelism
|
parCh chan struct{} // Semaphore used to limit parallelism
|
||||||
|
@ -50,6 +52,8 @@ type ContextOpts struct {
|
||||||
Providers map[string]ResourceProviderFactory
|
Providers map[string]ResourceProviderFactory
|
||||||
Provisioners map[string]ResourceProvisionerFactory
|
Provisioners map[string]ResourceProvisionerFactory
|
||||||
Variables map[string]string
|
Variables map[string]string
|
||||||
|
|
||||||
|
UIInput UIInput
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a new context.
|
// NewContext creates a new context.
|
||||||
|
@ -81,6 +85,7 @@ func NewContext(opts *ContextOpts) *Context {
|
||||||
providers: opts.Providers,
|
providers: opts.Providers,
|
||||||
provisioners: opts.Provisioners,
|
provisioners: opts.Provisioners,
|
||||||
variables: opts.Variables,
|
variables: opts.Variables,
|
||||||
|
uiInput: opts.UIInput,
|
||||||
|
|
||||||
parCh: parCh,
|
parCh: parCh,
|
||||||
sh: sh,
|
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.
|
// Plan generates an execution plan for the given context.
|
||||||
//
|
//
|
||||||
// The execution plan encapsulates the context and can be stored
|
// The execution plan encapsulates the context and can be stored
|
||||||
|
@ -337,6 +410,7 @@ type walkOperation byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
walkInvalid walkOperation = iota
|
walkInvalid walkOperation = iota
|
||||||
|
walkInput
|
||||||
walkApply
|
walkApply
|
||||||
walkPlan
|
walkPlan
|
||||||
walkPlanDestroy
|
walkPlanDestroy
|
||||||
|
@ -366,6 +440,8 @@ func (c *walkContext) Walk() error {
|
||||||
|
|
||||||
var walkFn depgraph.WalkFunc
|
var walkFn depgraph.WalkFunc
|
||||||
switch c.Operation {
|
switch c.Operation {
|
||||||
|
case walkInput:
|
||||||
|
walkFn = c.inputWalkFn()
|
||||||
case walkApply:
|
case walkApply:
|
||||||
walkFn = c.applyWalkFn()
|
walkFn = c.applyWalkFn()
|
||||||
case walkPlan:
|
case walkPlan:
|
||||||
|
@ -384,8 +460,11 @@ func (c *walkContext) Walk() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Operation == walkValidate {
|
switch c.Operation {
|
||||||
// Validation is the only one that doesn't calculate outputs
|
case walkInput:
|
||||||
|
fallthrough
|
||||||
|
case walkValidate:
|
||||||
|
// Don't calculate outputs
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +518,67 @@ func (c *walkContext) Walk() error {
|
||||||
return nil
|
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 {
|
func (c *walkContext) applyWalkFn() depgraph.WalkFunc {
|
||||||
cb := func(c *walkContext, r *Resource) error {
|
cb := func(c *walkContext, r *Resource) error {
|
||||||
var err error
|
var err error
|
||||||
|
@ -1385,6 +1525,12 @@ func (c *walkContext) computeResourceMultiVariable(
|
||||||
return strings.Join(values, ","), nil
|
return strings.Join(values, ","), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type walkInputMeta struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
Done map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
type walkValidateMeta struct {
|
type walkValidateMeta struct {
|
||||||
Errs []error
|
Errs []error
|
||||||
Warns []string
|
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) {
|
func TestContextApply(t *testing.T) {
|
||||||
m := testModule(t, "apply-good")
|
m := testModule(t, "apply-good")
|
||||||
p := testProvider("aws")
|
p := testProvider("aws")
|
||||||
|
|
|
@ -113,6 +113,19 @@ func (h *HookRecordApplyOrder) PreApply(
|
||||||
// Below are all the constant strings that are the expected output for
|
// Below are all the constant strings that are the expected output for
|
||||||
// various tests.
|
// 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 = `
|
const testTerraformApplyStr = `
|
||||||
aws_instance.bar:
|
aws_instance.bar:
|
||||||
ID = foo
|
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