Merge pull request #336 from hashicorp/f-input
Ask for input for variables and provider configs
This commit is contained in:
commit
5e9b609dac
|
@ -19,21 +19,25 @@ func Provider() *schema.Provider {
|
|||
return &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"region": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DefaultFunc: envDefaultFunc("AWS_REGION"),
|
||||
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.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -82,6 +82,7 @@ func (c *Client) ResourceProvider() (terraform.ResourceProvider, error) {
|
|||
}
|
||||
|
||||
return &ResourceProvider{
|
||||
Broker: c.broker,
|
||||
Client: rpc.NewClient(conn),
|
||||
Name: "ResourceProvider",
|
||||
}, nil
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package terraform
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -24,13 +25,15 @@ type genericWalkFunc func(*walkContext, *Resource) error
|
|||
//
|
||||
// Additionally, a context can be created from a Plan using Plan.Context.
|
||||
type Context struct {
|
||||
module *module.Tree
|
||||
diff *Diff
|
||||
hooks []Hook
|
||||
state *State
|
||||
providers map[string]ResourceProviderFactory
|
||||
provisioners map[string]ResourceProvisionerFactory
|
||||
variables map[string]string
|
||||
module *module.Tree
|
||||
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.
|
||||
|
@ -74,13 +79,15 @@ func NewContext(opts *ContextOpts) *Context {
|
|||
parCh := make(chan struct{}, par)
|
||||
|
||||
return &Context{
|
||||
diff: opts.Diff,
|
||||
hooks: hooks,
|
||||
module: opts.Module,
|
||||
state: opts.State,
|
||||
providers: opts.Providers,
|
||||
provisioners: opts.Provisioners,
|
||||
variables: opts.Variables,
|
||||
diff: opts.Diff,
|
||||
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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -119,7 +119,8 @@ type graphSharedProvider struct {
|
|||
ProviderKeys []string
|
||||
Parent *graphSharedProvider
|
||||
|
||||
parentNoun *depgraph.Noun
|
||||
overrideConfig map[string]map[string]interface{}
|
||||
parentNoun *depgraph.Noun
|
||||
}
|
||||
|
||||
// Graph builds a dependency graph of all the resources for infrastructure
|
||||
|
@ -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.
|
||||
|
|
|
@ -199,11 +199,16 @@ func (c *ResourceConfig) interpolate(ctx *walkContext) error {
|
|||
}
|
||||
}
|
||||
|
||||
if c.raw != nil {
|
||||
c.ComputedKeys = c.raw.UnknownKeys()
|
||||
c.Raw = c.raw.Raw
|
||||
c.Config = c.raw.Config()
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
resource "aws_instance" "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,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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue