Allow the HCL input when prompted

We already accept HCL encoded input for -vars, and this expands that to
accept HCL when prompted for a value on the command line as well.
This commit is contained in:
James Bardin 2016-08-09 16:14:40 -04:00
parent 012090f2a3
commit 2e5791ab2b
5 changed files with 138 additions and 22 deletions

View File

@ -317,16 +317,18 @@ func (c *Context) Input(mode InputMode) error {
} }
} }
var valueType config.VariableType
v := m[n] v := m[n]
switch v.Type() { switch valueType = v.Type(); valueType {
case config.VariableTypeUnknown: case config.VariableTypeUnknown:
continue continue
case config.VariableTypeMap: case config.VariableTypeMap:
continue // OK
case config.VariableTypeList: case config.VariableTypeList:
continue // OK
case config.VariableTypeString: case config.VariableTypeString:
// Good! // OK
default: default:
panic(fmt.Sprintf("Unknown variable type: %#v", v.Type())) panic(fmt.Sprintf("Unknown variable type: %#v", v.Type()))
} }
@ -340,6 +342,12 @@ func (c *Context) Input(mode InputMode) error {
} }
} }
// this should only happen during tests
if c.uiInput == nil {
log.Println("[WARN] Content.uiInput is nil")
continue
}
// Ask the user for a value for this variable // Ask the user for a value for this variable
var value string var value string
retry := 0 retry := 0
@ -355,27 +363,33 @@ func (c *Context) Input(mode InputMode) error {
"Error asking for %s: %s", n, err) "Error asking for %s: %s", n, err)
} }
if value == "" && v.Required() {
// Redo if it is required, but abort if we keep getting
// blank entries
if retry > 2 {
return fmt.Errorf("missing required value for %q", n)
}
retry++
continue
}
if value == "" { if value == "" {
// No value, just exit the loop. With no value, we just if v.Required() {
// use whatever is currently set in variables. // Redo if it is required, but abort if we keep getting
break // blank entries
if retry > 2 {
return fmt.Errorf("missing required value for %q", n)
}
retry++
continue
}
} }
break break
} }
if value != "" { // no value provided, so don't set the variable at all
c.variables[n] = value if value == "" {
continue
}
decoded, err := parseVariableAsHCL(n, value, valueType)
if err != nil {
return err
}
if decoded != nil {
c.variables[n] = decoded
} }
} }
} }
@ -656,9 +670,20 @@ func (c *Context) walk(
// the name of the variable. In order to get around the restriction of HCL requiring a // the name of the variable. In order to get around the restriction of HCL requiring a
// top level object, we prepend a sentinel key, decode the user-specified value as its // top level object, we prepend a sentinel key, decode the user-specified value as its
// value and pull the value back out of the resulting map. // value and pull the value back out of the resulting map.
func parseVariableAsHCL(name string, input interface{}, targetType config.VariableType) (interface{}, error) { func parseVariableAsHCL(name string, input string, targetType config.VariableType) (interface{}, error) {
// expecting a string so don't decode anything, just strip quotes
if targetType == config.VariableTypeString { if targetType == config.VariableTypeString {
return input, nil return strings.Trim(input, `"`), nil
}
// return empty types
if strings.TrimSpace(input) == "" {
switch targetType {
case config.VariableTypeList:
return []interface{}{}, nil
case config.VariableTypeMap:
return make(map[string]interface{}), nil
}
} }
const sentinelValue = "SENTINEL_TERRAFORM_VAR_OVERRIDE_KEY" const sentinelValue = "SENTINEL_TERRAFORM_VAR_OVERRIDE_KEY"

View File

@ -617,3 +617,44 @@ func TestContext2Input_interpolateVar(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
} }
func TestContext2Input_hcl(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "input-hcl")
p := testProvider("hcl")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"hcl": testProviderFuncFixed(p),
},
Variables: map[string]interface{}{},
UIInput: input,
})
input.InputReturnMap = map[string]string{
"var.listed": `["a", "b"]`,
"var.mapped": `{x = "y", w = "z"}`,
}
if err := ctx.Input(InputModeVar | InputModeVarUnset); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
state, err := ctx.Apply()
if err != nil {
t.Fatalf("err: %s", err)
}
actualStr := strings.TrimSpace(state.String())
expectedStr := strings.TrimSpace(testTerraformInputHCL)
if actualStr != expectedStr {
t.Logf("expected: \n%s", expectedStr)
t.Fatalf("bad: \n%s", actualStr)
}
}

View File

@ -6,6 +6,8 @@ import (
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/hashicorp/terraform/flatmap"
) )
func TestNewContextState(t *testing.T) { func TestNewContextState(t *testing.T) {
@ -172,7 +174,16 @@ func testDiffFn(
if reflect.DeepEqual(v, []interface{}{}) { if reflect.DeepEqual(v, []interface{}{}) {
attrDiff.New = "" attrDiff.New = ""
} else { } else {
attrDiff.New = v.(string) if s, ok := v.(string); ok {
attrDiff.New = s
} else {
// the value is something other than a string
// flatmap it, adding the diff for each value.
for k, attrDiff := range testFlatAttrDiffs(k, v) {
diff.Attributes[k] = attrDiff
}
continue
}
} }
if k == "require_new" { if k == "require_new" {
@ -219,6 +230,22 @@ func testDiffFn(
return diff, nil return diff, nil
} }
// generate ResourceAttrDiffs for nested data structures in tests
func testFlatAttrDiffs(k string, i interface{}) map[string]*ResourceAttrDiff {
flat := flatmap.Flatten(map[string]interface{}{k: i})
diffs := make(map[string]*ResourceAttrDiff)
for k, v := range flat {
attrDiff := &ResourceAttrDiff{
Old: "",
New: v,
}
diffs[k] = attrDiff
}
return diffs
}
func testProvider(prefix string) *MockResourceProvider { func testProvider(prefix string) *MockResourceProvider {
p := new(MockResourceProvider) p := new(MockResourceProvider)
p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) { p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) {

View File

@ -1413,3 +1413,14 @@ module.mod2:
STATE: STATE:
<no state>` <no state>`
const testTerraformInputHCL = `
hcl_instance.hcltest:
ID = foo
bar.w = z
bar.x = y
foo.# = 2
foo.0 = a
foo.1 = b
type = hcl_instance
`

View File

@ -0,0 +1,12 @@
variable "mapped" {
type = "map"
}
variable "listed" {
type = "list"
}
resource "hcl_instance" "hcltest" {
foo = "${var.listed}"
bar = "${var.mapped}"
}