Merge pull request #336 from hashicorp/f-input
Ask for input for variables and provider configs
This commit is contained in:
commit
5e9b609dac
|
@ -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.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 (
|
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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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