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, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: envDefaultFunc("AWS_REGION"), DefaultFunc: envDefaultFunc("AWS_REGION"),
Description: descriptions["region"],
InputDefault: "us-east-1",
}, },
"access_key": &schema.Schema{ "access_key": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: envDefaultFunc("AWS_ACCESS_KEY"), DefaultFunc: envDefaultFunc("AWS_ACCESS_KEY"),
Description: descriptions["access_key"],
}, },
"secret_key": &schema.Schema{ "secret_key": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
DefaultFunc: envDefaultFunc("AWS_SECRET_KEY"), DefaultFunc: envDefaultFunc("AWS_SECRET_KEY"),
Description: descriptions["secret_key"],
}, },
}, },
@ -55,3 +59,18 @@ func envDefaultFunc(k string) schema.SchemaDefaultFunc {
return nil, nil 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 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return Provider().Validate(c) return Provider().Validate(c)
} }

View File

@ -14,6 +14,12 @@ type ResourceProvider struct {
client *cloudflare.Client 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{ v := &config.Validator{
Required: []string{ Required: []string{

View File

@ -13,6 +13,12 @@ type ResourceProvider struct {
client *consulapi.Client 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{ v := &config.Validator{
Optional: []string{ Optional: []string{

View File

@ -19,6 +19,12 @@ type ResourceProvider struct {
p *schema.Provider 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
prov := Provider() prov := Provider()
return prov.Validate(c) return prov.Validate(c)

View File

@ -14,6 +14,12 @@ type ResourceProvider struct {
client *dnsimple.Client 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
v := &config.Validator{ v := &config.Validator{
Required: []string{ Required: []string{

View File

@ -76,6 +76,12 @@ func (c *ApplyCommand) Run(args []string) int {
c.Ui.Error(err.Error()) c.Ui.Error(err.Error())
return 1 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) { if !validateContext(ctx, c.Ui) {
return 1 return 1
} }
@ -234,6 +240,8 @@ Options:
modifying. Defaults to the "-state-out" path with modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup. ".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. -no-color If specified, output won't contain any color.
-refresh=true Update state prior to checking for differences. This -refresh=true Update state prior to checking for differences. This

View File

@ -7,6 +7,9 @@ import (
"github.com/mitchellh/cli" "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. // DefaultStateFilename is the default filename used for the state file.
const DefaultStateFilename = "terraform.tfstate" const DefaultStateFilename = "terraform.tfstate"

View File

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

View File

@ -30,6 +30,7 @@ type Meta struct {
// Variables for the context (private) // Variables for the context (private)
autoKey string autoKey string
autoVariables map[string]string autoVariables map[string]string
input bool
variables map[string]string variables map[string]string
color bool color bool
@ -105,6 +106,11 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return ctx, false, nil 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 // contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta. // context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts { func (m *Meta) contextOpts() *terraform.ContextOpts {
@ -127,6 +133,9 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
vs[k] = v vs[k] = v
} }
opts.Variables = vs opts.Variables = vs
opts.UIInput = &UIInput{
Colorize: m.Colorize(),
}
return &opts return &opts
} }
@ -134,6 +143,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
// flags adds the meta flags to the given FlagSet. // flags adds the meta flags to the given FlagSet.
func (m *Meta) flagSet(n string) *flag.FlagSet { func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError) f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagVar)(&m.variables), "var", "variables") f.Var((*FlagVar)(&m.variables), "var", "variables")
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file") 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") 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()) c.Ui.Error(err.Error())
return 1 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) { if !validateContext(ctx, c.Ui) {
return 1 return 1
} }
@ -167,6 +173,8 @@ Options:
-destroy If set, a plan will be generated to destroy all resources -destroy If set, a plan will be generated to destroy all resources
managed by the given configuration and state. 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. -module-depth=n Specifies the depth of modules to show in the output.
This does not affect the plan itself, only the output This does not affect the plan itself, only the output
shown. By default, this is zero. -1 will expand all. 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()) c.Ui.Error(err.Error())
return 1 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) { if !validateContext(ctx, c.Ui) {
return 1 return 1
} }
@ -147,6 +153,8 @@ Options:
modifying. Defaults to the "-state-out" path with modifying. Defaults to the "-state-out" path with
".backup" extension. Set to "-" to disable backup. ".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. -no-color If specified, output won't contain any color.
-state=path Path to read and save state (unless state-out -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 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. // Validate implementation of terraform.ResourceProvider interface.
func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) { func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(p.Schema).Validate(c) return schemaMap(p.Schema).Validate(c)

View File

@ -14,6 +14,7 @@ package schema
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -74,6 +75,14 @@ type Schema struct {
Default interface{} Default interface{}
DefaultFunc SchemaDefaultFunc 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. // The fields below relate to diffs.
// //
// If Computed is true, then the result of this value is computed // If Computed is true, then the result of this value is computed
@ -270,6 +279,55 @@ func (m schemaMap) Diff(
return result, nil 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. // Validate validates the configuration against this schema mapping.
func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return m.validateObject("", m, c) return m.validateObject("", m, c)
@ -569,6 +627,20 @@ func (m schemaMap) diffString(
return nil 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( func (m schemaMap) validate(
k string, k string,
schema *Schema, 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) { func TestSchemaMap_InternalValidate(t *testing.T) {
cases := []struct { cases := []struct {
In map[string]*Schema In map[string]*Schema

View File

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

View File

@ -9,10 +9,37 @@ import (
// ResourceProvider is an implementation of terraform.ResourceProvider // ResourceProvider is an implementation of terraform.ResourceProvider
// that communicates over RPC. // that communicates over RPC.
type ResourceProvider struct { type ResourceProvider struct {
Broker *muxBroker
Client *rpc.Client Client *rpc.Client
Name string 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) { func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
var resp ResourceProviderValidateResponse var resp ResourceProviderValidateResponse
args := ResourceProviderValidateArgs{ args := ResourceProviderValidateArgs{
@ -150,6 +177,7 @@ func (p *ResourceProvider) Resources() []terraform.ResourceType {
// ResourceProviderServer is a net/rpc compatible structure for serving // ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly. // a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct { type ResourceProviderServer struct {
Broker *muxBroker
Provider terraform.ResourceProvider Provider terraform.ResourceProvider
} }
@ -157,6 +185,16 @@ type ResourceProviderConfigureResponse struct {
Error *BasicError Error *BasicError
} }
type ResourceProviderInputArgs struct {
InputId uint32
Config *terraform.ResourceConfig
}
type ResourceProviderInputResponse struct {
Config *terraform.ResourceConfig
Error *BasicError
}
type ResourceProviderApplyArgs struct { type ResourceProviderApplyArgs struct {
Info *terraform.InstanceInfo Info *terraform.InstanceInfo
State *terraform.InstanceState State *terraform.InstanceState
@ -208,6 +246,33 @@ type ResourceProviderValidateResourceResponse struct {
Errors []*BasicError 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( func (s *ResourceProviderServer) Validate(
args *ResourceProviderValidateArgs, args *ResourceProviderValidateArgs,
reply *ResourceProviderValidateResponse) error { reply *ResourceProviderValidateResponse) error {

View File

@ -12,14 +12,54 @@ func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider) var _ terraform.ResourceProvider = new(ResourceProvider)
} }
func TestResourceProvider_configure(t *testing.T) { func TestResourceProvider_input(t *testing.T) {
p := new(terraform.MockResourceProvider) client, server := testNewClientServer(t)
client, server := testClientServer(t) defer client.Close()
name, err := Register(server, p)
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 { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
provider := &ResourceProvider{Client: client, Name: name}
// Configure // Configure
config := &terraform.ResourceConfig{ config := &terraform.ResourceConfig{

View File

@ -46,6 +46,24 @@ func testClientServer(t *testing.T) (*rpc.Client, *rpc.Server) {
return client, 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 { func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider { return func() terraform.ResourceProvider {
return p return p

View File

@ -96,7 +96,8 @@ func (d *dispenseServer) ResourceProvider(
return return
} }
d.serve(conn, "ResourceProvider", &ResourceProviderServer{ serve(conn, "ResourceProvider", &ResourceProviderServer{
Broker: d.broker,
Provider: d.ProviderFunc(), Provider: d.ProviderFunc(),
}) })
}() }()
@ -116,7 +117,7 @@ func (d *dispenseServer) ResourceProvisioner(
return return
} }
d.serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{ serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
Provisioner: d.ProvisionerFunc(), Provisioner: d.ProvisionerFunc(),
}) })
}() }()
@ -124,7 +125,17 @@ func (d *dispenseServer) ResourceProvisioner(
return nil 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() server := rpc.NewServer()
if err := server.RegisterName(name, v); err != nil { if err := server.RegisterName(name, v); err != nil {
log.Printf("[ERR] Plugin dispense: %s", err) 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 ( import (
"fmt" "fmt"
"log" "log"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -28,9 +29,11 @@ type Context struct {
diff *Diff diff *Diff
hooks []Hook hooks []Hook
state *State state *State
providerConfig map[string]map[string]map[string]interface{}
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 +53,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.
@ -78,9 +83,11 @@ func NewContext(opts *ContextOpts) *Context {
hooks: hooks, hooks: hooks,
module: opts.Module, module: opts.Module,
state: opts.State, state: opts.State,
providerConfig: make(map[string]map[string]map[string]interface{}),
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 +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. // 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 +412,7 @@ type walkOperation byte
const ( const (
walkInvalid walkOperation = iota walkInvalid walkOperation = iota
walkInput
walkApply walkApply
walkPlan walkPlan
walkPlanDestroy walkPlanDestroy
@ -366,6 +442,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 +462,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 +520,87 @@ 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:
// 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 { 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
@ -860,45 +1022,17 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc {
case *GraphNodeResourceProvider: case *GraphNodeResourceProvider:
sharedProvider := rn.Provider sharedProvider := rn.Provider
var raw *config.RawConfig // Check if we have an override
if sharedProvider.Config != nil { cs, ok := c.Context.providerConfig[rn.ID]
raw = sharedProvider.Config.RawConfig 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 { 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) log.Printf("[INFO] Validating provider: %s", k)
ws, es := p.Validate(rc) ws, es := p.Validate(rc)
for i, w := range ws { for i, w := range ws {
@ -976,47 +1110,17 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
case *GraphNodeResourceProvider: case *GraphNodeResourceProvider:
sharedProvider := m.Provider sharedProvider := m.Provider
// Interpolate in the variables and configure all the providers // Check if we have an override
var raw *config.RawConfig cs, ok := c.Context.providerConfig[m.ID]
if sharedProvider.Config != nil { if !ok {
raw = sharedProvider.Config.RawConfig 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 { 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) log.Printf("[INFO] Configuring provider: %s", k)
err := p.Configure(rc) err := p.Configure(rc)
if err != nil { if err != nil {
@ -1385,6 +1489,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

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) { func TestContextApply(t *testing.T) {
m := testModule(t, "apply-good") m := testModule(t, "apply-good")
p := testProvider("aws") p := testProvider("aws")

View File

@ -119,6 +119,7 @@ type graphSharedProvider struct {
ProviderKeys []string ProviderKeys []string
Parent *graphSharedProvider Parent *graphSharedProvider
overrideConfig map[string]map[string]interface{}
parentNoun *depgraph.Noun parentNoun *depgraph.Noun
} }
@ -1461,6 +1462,50 @@ func graphMapResourceProvisioners(g *depgraph.Graph,
return nil 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 // matchingPrefixes takes a resource type and a set of resource
// providers we know about by prefix and returns a list of prefixes // providers we know about by prefix and returns a list of prefixes
// that might be valid for that resource. // 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.ComputedKeys = c.raw.UnknownKeys()
c.Raw = c.raw.Raw c.Raw = c.raw.Raw
c.Config = c.raw.Config() c.Config = c.raw.Config()
}
return nil return nil
} }

View File

@ -4,6 +4,15 @@ package terraform
// resource provider: the thing that creates and manages the resources in // resource provider: the thing that creates and manages the resources in
// a Terraform configuration. // a Terraform configuration.
type ResourceProvider interface { 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 // Validate is called once at the beginning with the raw configuration
// (no interpolation done) and can return a list of warnings and/or // (no interpolation done) and can return a list of warnings and/or
// errors. // errors.

View File

@ -12,6 +12,12 @@ type MockResourceProvider struct {
// Anything you want, in case you need to store extra data with the mock. // Anything you want, in case you need to store extra data with the mock.
Meta interface{} Meta interface{}
InputCalled bool
InputInput UIInput
InputConfig *ResourceConfig
InputReturnConfig *ResourceConfig
InputReturnError error
InputFn func(UIInput, *ResourceConfig) (*ResourceConfig, error)
ApplyCalled bool ApplyCalled bool
ApplyInfo *InstanceInfo ApplyInfo *InstanceInfo
ApplyState *InstanceState ApplyState *InstanceState
@ -51,6 +57,17 @@ type MockResourceProvider struct {
ValidateResourceReturnErrors []error 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) { func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) {
p.Lock() p.Lock()
defer p.Unlock() 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 // Below are all the constant strings that are the expected output for
// various tests. // 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 = ` const testTerraformApplyStr = `
aws_instance.bar: aws_instance.bar:
ID = foo 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)
}
}