Merge pull request #336 from hashicorp/f-input

Ask for input for variables and provider configs
This commit is contained in:
Mitchell Hashimoto 2014-09-29 14:12:49 -07:00
commit 5e9b609dac
38 changed files with 1260 additions and 106 deletions

View File

@ -22,18 +22,22 @@ func Provider() *schema.Provider {
Type: schema.TypeString,
Required: true,
DefaultFunc: envDefaultFunc("AWS_REGION"),
Description: descriptions["region"],
InputDefault: "us-east-1",
},
"access_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: envDefaultFunc("AWS_ACCESS_KEY"),
Description: descriptions["access_key"],
},
"secret_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: envDefaultFunc("AWS_SECRET_KEY"),
Description: descriptions["secret_key"],
},
},
@ -55,3 +59,18 @@ func envDefaultFunc(k string) schema.SchemaDefaultFunc {
return nil, nil
}
}
var descriptions map[string]string
func init() {
descriptions = map[string]string{
"region": "The region where AWS operations will take place. Examples\n" +
"are us-east-1, us-west-2, etc.",
"access_key": "The access key for API operations. You can retrieve this\n" +
"from the 'Security & Credentials' section of the AWS console.",
"secret_key": "The secret key for API operations. You can retrieve this\n" +
"from the 'Security & Credentials' section of the AWS console.",
}
}

View File

@ -30,6 +30,12 @@ type ResourceProvider struct {
p *schema.Provider
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return Provider().Input(input, c)
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return Provider().Validate(c)
}

View File

@ -14,6 +14,12 @@ type ResourceProvider struct {
client *cloudflare.Client
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return c, nil
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{
Required: []string{

View File

@ -13,6 +13,12 @@ type ResourceProvider struct {
client *consulapi.Client
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return c, nil
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{
Optional: []string{

View File

@ -19,6 +19,12 @@ type ResourceProvider struct {
p *schema.Provider
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return Provider().Input(input, c)
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
prov := Provider()
return prov.Validate(c)

View File

@ -14,6 +14,12 @@ type ResourceProvider struct {
client *dnsimple.Client
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return c, nil
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{
Required: []string{

View File

@ -76,6 +76,12 @@ func (c *ApplyCommand) Run(args []string) int {
c.Ui.Error(err.Error())
return 1
}
if c.Input() {
if err := ctx.Input(); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
}
if !validateContext(ctx, c.Ui) {
return 1
}
@ -234,6 +240,8 @@ Options:
modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup.
-input=true Ask for input for variables if not directly set.
-no-color If specified, output won't contain any color.
-refresh=true Update state prior to checking for differences. This

View File

@ -7,6 +7,9 @@ import (
"github.com/mitchellh/cli"
)
// Set to true when we're testing
var test bool = false
// DefaultStateFilename is the default filename used for the state file.
const DefaultStateFilename = "terraform.tfstate"

View File

@ -14,6 +14,8 @@ import (
var fixtureDir = "./test-fixtures"
func init() {
test = true
// Expand the fixture dir on init because we change the working
// directory in some tests.
var err error

View File

@ -30,6 +30,7 @@ type Meta struct {
// Variables for the context (private)
autoKey string
autoVariables map[string]string
input bool
variables map[string]string
color bool
@ -105,6 +106,11 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return ctx, false, nil
}
// Input returns true if we should ask for input for context.
func (m *Meta) Input() bool {
return !test && m.input && len(m.variables) == 0
}
// contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts {
@ -127,6 +133,9 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
vs[k] = v
}
opts.Variables = vs
opts.UIInput = &UIInput{
Colorize: m.Colorize(),
}
return &opts
}
@ -134,6 +143,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
// flags adds the meta flags to the given FlagSet.
func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagVar)(&m.variables), "var", "variables")
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")

View File

@ -47,3 +47,54 @@ func TestMetaColorize(t *testing.T) {
t.Fatal("should be disabled")
}
}
func TestMetaInput(t *testing.T) {
test = false
defer func() { test = true }()
m := new(Meta)
args := []string{}
fs := m.flagSet("foo")
if err := fs.Parse(args); err != nil {
t.Fatalf("err: %s", err)
}
if !m.Input() {
t.Fatal("should input")
}
}
func TestMetaInput_disable(t *testing.T) {
test = false
defer func() { test = true }()
m := new(Meta)
args := []string{"-input=false"}
fs := m.flagSet("foo")
if err := fs.Parse(args); err != nil {
t.Fatalf("err: %s", err)
}
if m.Input() {
t.Fatal("should not input")
}
}
func TestMetaInput_vars(t *testing.T) {
test = false
defer func() { test = true }()
m := new(Meta)
args := []string{"-var", "foo=bar"}
fs := m.flagSet("foo")
if err := fs.Parse(args); err != nil {
t.Fatalf("err: %s", err)
}
if m.Input() {
t.Fatal("should not input")
}
}

View File

@ -75,6 +75,12 @@ func (c *PlanCommand) Run(args []string) int {
c.Ui.Error(err.Error())
return 1
}
if c.Input() {
if err := ctx.Input(); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
}
if !validateContext(ctx, c.Ui) {
return 1
}
@ -167,6 +173,8 @@ Options:
-destroy If set, a plan will be generated to destroy all resources
managed by the given configuration and state.
-input=true Ask for input for variables if not directly set.
-module-depth=n Specifies the depth of modules to show in the output.
This does not affect the plan itself, only the output
shown. By default, this is zero. -1 will expand all.

View File

@ -92,6 +92,12 @@ func (c *RefreshCommand) Run(args []string) int {
c.Ui.Error(err.Error())
return 1
}
if c.Input() {
if err := ctx.Input(); err != nil {
c.Ui.Error(fmt.Sprintf("Error configuring: %s", err))
return 1
}
}
if !validateContext(ctx, c.Ui) {
return 1
}
@ -147,6 +153,8 @@ Options:
modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup.
-input=true Ask for input for variables if not directly set.
-no-color If specified, output won't contain any color.
-state=path Path to read and save state (unless state-out

134
command/ui_input.go Normal file
View File

@ -0,0 +1,134 @@
package command
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"os/signal"
"strings"
"sync"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/colorstring"
)
var defaultInputReader io.Reader
var defaultInputWriter io.Writer
// UIInput is an implementation of terraform.UIInput that asks the CLI
// for input stdin.
type UIInput struct {
// Colorize will color the output.
Colorize *colorstring.Colorize
// Reader and Writer for IO. If these aren't set, they will default to
// Stdout and Stderr respectively.
Reader io.Reader
Writer io.Writer
interrupted bool
l sync.Mutex
once sync.Once
}
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
i.once.Do(i.init)
r := i.Reader
w := i.Writer
if r == nil {
r = defaultInputReader
}
if w == nil {
w = defaultInputWriter
}
if r == nil {
r = os.Stdin
}
if w == nil {
w = os.Stdout
}
// Make sure we only ask for input once at a time. Terraform
// should enforce this, but it doesn't hurt to verify.
i.l.Lock()
defer i.l.Unlock()
// If we're interrupted, then don't ask for input
if i.interrupted {
return "", errors.New("interrupted")
}
// Listen for interrupts so we can cancel the input ask
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
defer signal.Stop(sigCh)
// Build the output format for asking
var buf bytes.Buffer
buf.WriteString("[reset]")
buf.WriteString(fmt.Sprintf("[bold]%s[reset]\n", opts.Query))
if opts.Description != "" {
s := bufio.NewScanner(strings.NewReader(opts.Description))
for s.Scan() {
buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
}
buf.WriteString("\n")
}
if opts.Default != "" {
buf.WriteString(" [bold]Default:[reset] ")
buf.WriteString(opts.Default)
buf.WriteString("\n")
}
buf.WriteString(" [bold]Enter a value:[reset] ")
// Ask the user for their input
if _, err := fmt.Fprint(w, i.Colorize.Color(buf.String())); err != nil {
return "", err
}
// Listen for the input in a goroutine. This will allow us to
// interrupt this if we are interrupted (SIGINT)
result := make(chan string, 1)
go func() {
var line string
if _, err := fmt.Fscanln(r, &line); err != nil {
log.Printf("[ERR] UIInput scan err: %s", err)
}
result <- line
}()
select {
case line := <-result:
fmt.Fprint(w, "\n")
if line == "" {
line = opts.Default
}
return line, nil
case <-sigCh:
// Print a newline so that any further output starts properly
// on a new line.
fmt.Fprintln(w)
// Mark that we were interrupted so future Ask calls fail.
i.interrupted = true
return "", errors.New("interrupted")
}
}
func (i *UIInput) init() {
if i.Colorize == nil {
i.Colorize = &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}
}

28
command/ui_input_test.go Normal file
View File

@ -0,0 +1,28 @@
package command
import (
"bytes"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestUIInput_impl(t *testing.T) {
var _ terraform.UIInput = new(UIInput)
}
func TestUIInputInput(t *testing.T) {
i := &UIInput{
Reader: bytes.NewBufferString("foo\n"),
Writer: bytes.NewBuffer(nil),
}
v, err := i.Input(&terraform.InputOpts{})
if err != nil {
t.Fatalf("err: %s", err)
}
if v != "foo" {
t.Fatalf("bad: %#v", v)
}
}

View File

@ -87,6 +87,13 @@ func (p *Provider) SetMeta(v interface{}) {
p.meta = v
}
// Input implementation of terraform.ResourceProvider interface.
func (p *Provider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
return schemaMap(p.Schema).Input(input, c)
}
// Validate implementation of terraform.ResourceProvider interface.
func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(p.Schema).Validate(c)

View File

@ -14,6 +14,7 @@ package schema
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
@ -74,6 +75,14 @@ type Schema struct {
Default interface{}
DefaultFunc SchemaDefaultFunc
// Description is used as the description for docs or asking for user
// input. It should be relatively short (a few sentences max) and should
// be formatted to fit a CLI.
Description string
// InputDefault is the default value to use for when inputs are requested.
InputDefault string
// The fields below relate to diffs.
//
// If Computed is true, then the result of this value is computed
@ -270,6 +279,55 @@ func (m schemaMap) Diff(
return result, nil
}
// Input implements the terraform.ResourceProvider method by asking
// for input for required configuration keys that don't have a value.
func (m schemaMap) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
keys := make([]string, 0, len(m))
for k, _ := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := m[k]
// Skip things that don't require config, if that is even valid
// for a provider schema.
if !v.Required && !v.Optional {
continue
}
// Skip things that have a value of some sort already
if _, ok := c.Raw[k]; ok {
continue
}
var value interface{}
var err error
switch v.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeString:
value, err = m.inputString(input, k, v)
default:
panic(fmt.Sprintf("Unknown type for input: %s", v.Type))
}
if err != nil {
return nil, fmt.Errorf(
"%s: %s", k, err)
}
c.Raw[k] = value
}
return c, nil
}
// Validate validates the configuration against this schema mapping.
func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return m.validateObject("", m, c)
@ -569,6 +627,20 @@ func (m schemaMap) diffString(
return nil
}
func (m schemaMap) inputString(
input terraform.UIInput,
k string,
schema *Schema) (interface{}, error) {
result, err := input.Input(&terraform.InputOpts{
Id: k,
Query: k,
Description: schema.Description,
Default: schema.InputDefault,
})
return result, err
}
func (m schemaMap) validate(
k string,
schema *Schema,

View File

@ -1035,6 +1035,86 @@ func TestSchemaMap_Diff(t *testing.T) {
}
}
func TestSchemaMap_Input(t *testing.T) {
cases := []struct {
Schema map[string]*Schema
Config map[string]interface{}
Input map[string]string
Result map[string]interface{}
Err bool
}{
/*
* String decode
*/
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
},
},
Input: map[string]string{
"availability_zone": "foo",
},
Result: map[string]interface{}{
"availability_zone": "foo",
},
Err: false,
},
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
},
},
Config: map[string]interface{}{
"availability_zone": "bar",
},
Input: map[string]string{
"availability_zone": "foo",
},
Result: map[string]interface{}{
"availability_zone": "bar",
},
Err: false,
},
}
for i, tc := range cases {
if tc.Config == nil {
tc.Config = make(map[string]interface{})
}
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
input := new(terraform.MockUIInput)
input.InputReturnMap = tc.Input
actual, err := schemaMap(tc.Schema).Input(
input, terraform.NewResourceConfig(c))
if (err != nil) != tc.Err {
t.Fatalf("#%d err: %s", i, err)
}
if !reflect.DeepEqual(tc.Result, actual.Raw) {
t.Fatalf("#%d: bad:\n\n%#v", i, actual.Raw)
}
}
}
func TestSchemaMap_InternalValidate(t *testing.T) {
cases := []struct {
In map[string]*Schema

View File

@ -82,6 +82,7 @@ func (c *Client) ResourceProvider() (terraform.ResourceProvider, error) {
}
return &ResourceProvider{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvider",
}, nil

View File

@ -9,10 +9,37 @@ import (
// ResourceProvider is an implementation of terraform.ResourceProvider
// that communicates over RPC.
type ResourceProvider struct {
Broker *muxBroker
Client *rpc.Client
Name string
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
id := p.Broker.NextId()
go acceptAndServe(p.Broker, id, "UIInput", &UIInputServer{
UIInput: input,
})
var resp ResourceProviderInputResponse
args := ResourceProviderInputArgs{
InputId: id,
Config: c,
}
err := p.Client.Call(p.Name+".Input", &args, &resp)
if err != nil {
return nil, err
}
if resp.Error != nil {
err = resp.Error
return nil, err
}
return resp.Config, nil
}
func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
var resp ResourceProviderValidateResponse
args := ResourceProviderValidateArgs{
@ -150,6 +177,7 @@ func (p *ResourceProvider) Resources() []terraform.ResourceType {
// ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct {
Broker *muxBroker
Provider terraform.ResourceProvider
}
@ -157,6 +185,16 @@ type ResourceProviderConfigureResponse struct {
Error *BasicError
}
type ResourceProviderInputArgs struct {
InputId uint32
Config *terraform.ResourceConfig
}
type ResourceProviderInputResponse struct {
Config *terraform.ResourceConfig
Error *BasicError
}
type ResourceProviderApplyArgs struct {
Info *terraform.InstanceInfo
State *terraform.InstanceState
@ -208,6 +246,33 @@ type ResourceProviderValidateResourceResponse struct {
Errors []*BasicError
}
func (s *ResourceProviderServer) Input(
args *ResourceProviderInputArgs,
reply *ResourceProviderInputResponse) error {
conn, err := s.Broker.Dial(args.InputId)
if err != nil {
*reply = ResourceProviderInputResponse{
Error: NewBasicError(err),
}
return nil
}
client := rpc.NewClient(conn)
defer client.Close()
input := &UIInput{
Client: client,
Name: "UIInput",
}
config, err := s.Provider.Input(input, args.Config)
*reply = ResourceProviderInputResponse{
Config: config,
Error: NewBasicError(err),
}
return nil
}
func (s *ResourceProviderServer) Validate(
args *ResourceProviderValidateArgs,
reply *ResourceProviderValidateResponse) error {

View File

@ -12,14 +12,54 @@ func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider)
}
func TestResourceProvider_configure(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
func TestResourceProvider_input(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
input := new(terraform.MockUIInput)
expected := &terraform.ResourceConfig{
Raw: map[string]interface{}{"bar": "baz"},
}
p.InputReturnConfig = expected
// Input
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
actual, err := provider.Input(input, config)
if !p.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(p.InputConfig, config) {
t.Fatalf("bad: %#v", p.InputConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceProvider_configure(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{

View File

@ -46,6 +46,24 @@ func testClientServer(t *testing.T) (*rpc.Client, *rpc.Server) {
return client, server
}
func testNewClientServer(t *testing.T) (*Client, *Server) {
clientConn, serverConn := testConn(t)
server := &Server{
ProviderFunc: testProviderFixed(new(terraform.MockResourceProvider)),
ProvisionerFunc: testProvisionerFixed(
new(terraform.MockResourceProvisioner)),
}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
return client, server
}
func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider {
return p

View File

@ -96,7 +96,8 @@ func (d *dispenseServer) ResourceProvider(
return
}
d.serve(conn, "ResourceProvider", &ResourceProviderServer{
serve(conn, "ResourceProvider", &ResourceProviderServer{
Broker: d.broker,
Provider: d.ProviderFunc(),
})
}()
@ -116,7 +117,7 @@ func (d *dispenseServer) ResourceProvisioner(
return
}
d.serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
Provisioner: d.ProvisionerFunc(),
})
}()
@ -124,7 +125,17 @@ func (d *dispenseServer) ResourceProvisioner(
return nil
}
func (d *dispenseServer) serve(conn io.ReadWriteCloser, name string, v interface{}) {
func acceptAndServe(mux *muxBroker, id uint32, n string, v interface{}) {
conn, err := mux.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin acceptAndServe: %s", err)
return
}
serve(conn, n, v)
}
func serve(conn io.ReadWriteCloser, name string, v interface{}) {
server := rpc.NewServer()
if err := server.RegisterName(name, v); err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)

51
rpc/ui_input.go Normal file
View File

@ -0,0 +1,51 @@
package rpc
import (
"net/rpc"
"github.com/hashicorp/terraform/terraform"
)
// UIInput is an implementatin of terraform.UIInput that communicates
// over RPC.
type UIInput struct {
Client *rpc.Client
Name string
}
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
var resp UIInputInputResponse
err := i.Client.Call(i.Name+".Input", opts, &resp)
if err != nil {
return "", err
}
if resp.Error != nil {
err = resp.Error
return "", err
}
return resp.Value, nil
}
type UIInputInputResponse struct {
Value string
Error *BasicError
}
// UIInputServer is a net/rpc compatible structure for serving
// a UIInputServer. This should not be used directly.
type UIInputServer struct {
UIInput terraform.UIInput
}
func (s *UIInputServer) Input(
opts *terraform.InputOpts,
reply *UIInputInputResponse) error {
value, err := s.UIInput.Input(opts)
*reply = UIInputInputResponse{
Value: value,
Error: NewBasicError(err),
}
return nil
}

48
rpc/ui_input_test.go Normal file
View File

@ -0,0 +1,48 @@
package rpc
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestUIInput_impl(t *testing.T) {
var _ terraform.UIInput = new(UIInput)
}
func TestUIInput_input(t *testing.T) {
client, server := testClientServer(t)
defer client.Close()
i := new(terraform.MockUIInput)
i.InputReturnString = "foo"
err := server.RegisterName("UIInput", &UIInputServer{
UIInput: i,
})
if err != nil {
t.Fatalf("err: %s", err)
}
input := &UIInput{Client: client, Name: "UIInput"}
opts := &terraform.InputOpts{
Id: "foo",
}
v, err := input.Input(opts)
if !i.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(i.InputOpts, opts) {
t.Fatalf("bad: %#v", i.InputOpts)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if v != "foo" {
t.Fatalf("bad: %#v", v)
}
}

View File

@ -3,6 +3,7 @@ package terraform
import (
"fmt"
"log"
"sort"
"strconv"
"strings"
"sync"
@ -28,9 +29,11 @@ type Context struct {
diff *Diff
hooks []Hook
state *State
providerConfig map[string]map[string]map[string]interface{}
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 +53,8 @@ type ContextOpts struct {
Providers map[string]ResourceProviderFactory
Provisioners map[string]ResourceProvisionerFactory
Variables map[string]string
UIInput UIInput
}
// NewContext creates a new context.
@ -78,9 +83,11 @@ func NewContext(opts *ContextOpts) *Context {
hooks: hooks,
module: opts.Module,
state: opts.State,
providerConfig: make(map[string]map[string]map[string]interface{}),
providers: opts.Providers,
provisioners: opts.Provisioners,
variables: opts.Variables,
uiInput: opts.UIInput,
parCh: parCh,
sh: sh,
@ -126,6 +133,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("var.%s", n),
Description: v.Description,
})
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 +412,7 @@ type walkOperation byte
const (
walkInvalid walkOperation = iota
walkInput
walkApply
walkPlan
walkPlanDestroy
@ -366,6 +442,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 +462,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 +520,87 @@ 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:
// Acquire the lock the whole time so we only ask for input
// one at a time.
meta.Lock()
defer meta.Unlock()
// If we already did this provider, then we're done.
if _, ok := meta.Done[rn.ID]; 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)
// Wrap the input into a namespace
input := &PrefixUIInput{
IdPrefix: fmt.Sprintf("provider.%s", rn.ID),
QueryPrefix: fmt.Sprintf("provider.%s.", rn.ID),
UIInput: c.Context.uiInput,
}
// Go through each provider and capture the input necessary
// to satisfy it.
configs := make(map[string]map[string]interface{})
for k, p := range sharedProvider.Providers {
newc, err := p.Input(input, rc)
if err != nil {
return fmt.Errorf(
"Error configuring %s: %s", k, err)
}
if newc != nil {
configs[k] = newc.Raw
}
}
// Mark this provider as done
meta.Done[rn.ID] = struct{}{}
// Set the configuration
c.Context.providerConfig[rn.ID] = configs
}
return nil
}
}
func (c *walkContext) applyWalkFn() depgraph.WalkFunc {
cb := func(c *walkContext, r *Resource) error {
var err error
@ -860,45 +1022,17 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc {
case *GraphNodeResourceProvider:
sharedProvider := rn.Provider
var raw *config.RawConfig
if sharedProvider.Config != nil {
raw = sharedProvider.Config.RawConfig
// Check if we have an override
cs, ok := c.Context.providerConfig[rn.ID]
if !ok {
cs = make(map[string]map[string]interface{})
}
// If we have a parent, then merge in the parent configurations
// properly so we "inherit" the configurations.
if sharedProvider.Parent != nil {
var rawMap map[string]interface{}
if raw != nil {
rawMap = raw.Raw
}
parent := sharedProvider.Parent
for parent != nil {
if parent.Config != nil {
if rawMap == nil {
rawMap = parent.Config.RawConfig.Raw
}
for k, v := range parent.Config.RawConfig.Raw {
rawMap[k] = v
}
}
parent = parent.Parent
}
// Update our configuration to be the merged result
var err error
raw, err = config.NewRawConfig(rawMap)
if err != nil {
return fmt.Errorf("Error merging configurations: %s", err)
}
}
rc := NewResourceConfig(raw)
for k, p := range sharedProvider.Providers {
// Merge the configurations to get what we use to configure with
rc := sharedProvider.MergeConfig(false, cs[k])
rc.interpolate(c)
log.Printf("[INFO] Validating provider: %s", k)
ws, es := p.Validate(rc)
for i, w := range ws {
@ -976,47 +1110,17 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
case *GraphNodeResourceProvider:
sharedProvider := m.Provider
// Interpolate in the variables and configure all the providers
var raw *config.RawConfig
if sharedProvider.Config != nil {
raw = sharedProvider.Config.RawConfig
// Check if we have an override
cs, ok := c.Context.providerConfig[m.ID]
if !ok {
cs = make(map[string]map[string]interface{})
}
// If we have a parent, then merge in the parent configurations
// properly so we "inherit" the configurations.
if sharedProvider.Parent != nil {
var rawMap map[string]interface{}
if raw != nil {
rawMap = raw.Raw
}
parent := sharedProvider.Parent
for parent != nil {
if parent.Config != nil {
if rawMap == nil {
rawMap = parent.Config.RawConfig.Raw
}
for k, v := range parent.Config.RawConfig.Config() {
rawMap[k] = v
}
}
parent = parent.Parent
}
// Update our configuration to be the merged result
var err error
raw, err = config.NewRawConfig(rawMap)
if err != nil {
return fmt.Errorf("Error merging configurations: %s", err)
}
}
rc := NewResourceConfig(raw)
rc.interpolate(c)
for k, p := range sharedProvider.Providers {
// Merge the configurations to get what we use to configure with
rc := sharedProvider.MergeConfig(false, cs[k])
rc.interpolate(c)
log.Printf("[INFO] Configuring provider: %s", k)
err := p.Configure(rc)
if err != nil {
@ -1385,6 +1489,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,137 @@ 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 TestContextInput_provider(t *testing.T) {
m := testModule(t, "input-provider")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
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(); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Apply(); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, "bar") {
t.Fatalf("bad: %#v", actual)
}
}
func TestContextInput_providerId(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "input-provider")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
UIInput: input,
})
var actual interface{}
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
v, err := i.Input(&InputOpts{Id: "foo"})
if err != nil {
return nil, err
}
c.Raw["foo"] = v
return c, nil
}
p.ConfigureFn = func(c *ResourceConfig) error {
actual = c.Raw["foo"]
return nil
}
input.InputReturnMap = map[string]string{
"provider.aws.foo": "bar",
}
if err := ctx.Input(); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Apply(); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(actual, "bar") {
t.Fatalf("bad: %#v", actual)
}
}
func TestContextApply(t *testing.T) {
m := testModule(t, "apply-good")
p := testProvider("aws")

View File

@ -119,6 +119,7 @@ type graphSharedProvider struct {
ProviderKeys []string
Parent *graphSharedProvider
overrideConfig map[string]map[string]interface{}
parentNoun *depgraph.Noun
}
@ -1461,6 +1462,50 @@ func graphMapResourceProvisioners(g *depgraph.Graph,
return nil
}
// MergeConfig merges all the configurations in the proper order
// to result in the final configuration to use to configure this
// provider.
func (p *graphSharedProvider) MergeConfig(
raw bool, override map[string]interface{}) *ResourceConfig {
var rawMap map[string]interface{}
if override != nil {
rawMap = override
} else if p.Config != nil {
rawMap = p.Config.RawConfig.Raw
}
if rawMap == nil {
rawMap = make(map[string]interface{})
}
// Merge in all the parent configurations
if p.Parent != nil {
parent := p.Parent
for parent != nil {
if parent.Config != nil {
var merge map[string]interface{}
if raw {
merge = parent.Config.RawConfig.Raw
} else {
merge = parent.Config.RawConfig.Config()
}
for k, v := range merge {
rawMap[k] = v
}
}
parent = parent.Parent
}
}
rc, err := config.NewRawConfig(rawMap)
if err != nil {
panic("error building config: " + err.Error())
}
return NewResourceConfig(rc)
}
// matchingPrefixes takes a resource type and a set of resource
// providers we know about by prefix and returns a list of prefixes
// that might be valid for that resource.

View File

@ -199,11 +199,16 @@ func (c *ResourceConfig) interpolate(ctx *walkContext) error {
}
}
if c.raw != nil {
if c.raw == nil {
var err error
c.raw, err = config.NewRawConfig(make(map[string]interface{}))
if err != nil {
return err
}
}
c.ComputedKeys = c.raw.UnknownKeys()
c.Raw = c.raw.Raw
c.Config = c.raw.Config()
}
return nil
}

View File

@ -4,6 +4,15 @@ package terraform
// resource provider: the thing that creates and manages the resources in
// a Terraform configuration.
type ResourceProvider interface {
// Input is called to ask the provider to ask the user for input
// for completing the configuration if necesarry.
//
// This may or may not be called, so resource provider writers shouldn't
// rely on this being available to set some default values for validate
// later. Example of a situation where this wouldn't be called is if
// the user is not using a TTY.
Input(UIInput, *ResourceConfig) (*ResourceConfig, error)
// Validate is called once at the beginning with the raw configuration
// (no interpolation done) and can return a list of warnings and/or
// errors.

View File

@ -12,6 +12,12 @@ type MockResourceProvider struct {
// Anything you want, in case you need to store extra data with the mock.
Meta interface{}
InputCalled bool
InputInput UIInput
InputConfig *ResourceConfig
InputReturnConfig *ResourceConfig
InputReturnError error
InputFn func(UIInput, *ResourceConfig) (*ResourceConfig, error)
ApplyCalled bool
ApplyInfo *InstanceInfo
ApplyState *InstanceState
@ -51,6 +57,17 @@ type MockResourceProvider struct {
ValidateResourceReturnErrors []error
}
func (p *MockResourceProvider) Input(
input UIInput, c *ResourceConfig) (*ResourceConfig, error) {
p.InputCalled = true
p.InputInput = input
p.InputConfig = c
if p.InputFn != nil {
return p.InputFn(input, c)
}
return p.InputReturnConfig, p.InputReturnError
}
func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
p.Lock()
defer p.Unlock()

View File

@ -113,6 +113,32 @@ func (h *HookRecordApplyOrder) PreApply(
// Below are all the constant strings that are the expected output for
// various tests.
const testTerraformInputProviderStr = `
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 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 @@
resource "aws_instance" "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)}"
}

26
terraform/ui_input.go Normal file
View File

@ -0,0 +1,26 @@
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
// Description is a description about what this option is. Be wary
// that this will probably be in a terminal so split lines as you see
// necessary.
Description string
// Default will be the value returned if no data is entered.
Default 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
}

View File

@ -0,0 +1,19 @@
package terraform
import (
"fmt"
)
// PrefixUIInput is an implementation of UIInput that prefixes the ID
// with a string, allowing queries to be namespaced.
type PrefixUIInput struct {
IdPrefix string
QueryPrefix string
UIInput UIInput
}
func (i *PrefixUIInput) Input(opts *InputOpts) (string, error) {
opts.Id = fmt.Sprintf("%s.%s", i.IdPrefix, opts.Id)
opts.Query = fmt.Sprintf("%s%s", i.QueryPrefix, opts.Query)
return i.UIInput.Input(opts)
}

View File

@ -0,0 +1,26 @@
package terraform
import (
"testing"
)
func TestPrefixUIInput_impl(t *testing.T) {
var _ UIInput = new(PrefixUIInput)
}
func testPrefixUIInput(t *testing.T) {
input := new(MockUIInput)
prefix := &PrefixUIInput{
IdPrefix: "foo",
UIInput: input,
}
_, err := prefix.Input(&InputOpts{Id: "bar"})
if err != nil {
t.Fatalf("err: %s", err)
}
if input.InputOpts.Id != "foo.bar" {
t.Fatalf("bad: %#v", input.InputOpts)
}
}