Merge pull request #4795 from hashicorp/f-variable-types

core: Support explicit variable type declaration
This commit is contained in:
James Nugent 2016-01-25 10:37:43 -06:00
commit 327f11460c
11 changed files with 186 additions and 30 deletions

View File

@ -98,9 +98,10 @@ type Provisioner struct {
// Variable is a variable defined within the configuration. // Variable is a variable defined within the configuration.
type Variable struct { type Variable struct {
Name string Name string
Default interface{} DeclaredType string `mapstructure:"type"`
Description string Default interface{}
Description string
} }
// Output is an output defined within the configuration. An output is // Output is an output defined within the configuration. An output is
@ -177,7 +178,7 @@ func (c *Config) Validate() error {
for _, v := range c.Variables { for _, v := range c.Variables {
if v.Type() == VariableTypeUnknown { if v.Type() == VariableTypeUnknown {
errs = append(errs, fmt.Errorf( errs = append(errs, fmt.Errorf(
"Variable '%s': must be string or mapping", "Variable '%s': must be a string or a map",
v.Name)) v.Name))
continue continue
} }
@ -780,8 +781,64 @@ func (v *Variable) Merge(v2 *Variable) *Variable {
return &result return &result
} }
// Type returns the type of varialbe this is. var typeStringMap = map[string]VariableType{
"string": VariableTypeString,
"map": VariableTypeMap,
}
// Type returns the type of variable this is.
func (v *Variable) Type() VariableType { func (v *Variable) Type() VariableType {
if v.DeclaredType != "" {
declaredType, ok := typeStringMap[v.DeclaredType]
if !ok {
return VariableTypeUnknown
}
return declaredType
}
return v.inferTypeFromDefault()
}
// ValidateTypeAndDefault ensures that default variable value is compatible
// with the declared type (if one exists), and that the type is one which is
// known to Terraform
func (v *Variable) ValidateTypeAndDefault() error {
// If an explicit type is declared, ensure it is valid
if v.DeclaredType != "" {
if _, ok := typeStringMap[v.DeclaredType]; !ok {
return fmt.Errorf("Variable '%s' must be of type string or map - '%s' is not a valid type", v.Name, v.DeclaredType)
}
}
if v.DeclaredType == "" || v.Default == nil {
return nil
}
if v.inferTypeFromDefault() != v.Type() {
return fmt.Errorf("'%s' has a default value which is not of type '%s'", v.Name, v.DeclaredType)
}
return nil
}
func (v *Variable) mergerName() string {
return v.Name
}
func (v *Variable) mergerMerge(m merger) merger {
return v.Merge(m.(*Variable))
}
// Required tests whether a variable is required or not.
func (v *Variable) Required() bool {
return v.Default == nil
}
// inferTypeFromDefault contains the logic for the old method of inferring
// variable types - we can also use this for validating that the declared
// type matches the type of the default value
func (v *Variable) inferTypeFromDefault() VariableType {
if v.Default == nil { if v.Default == nil {
return VariableTypeString return VariableTypeString
} }
@ -800,16 +857,3 @@ func (v *Variable) Type() VariableType {
return VariableTypeUnknown return VariableTypeUnknown
} }
func (v *Variable) mergerName() string {
return v.Name
}
func (v *Variable) mergerMerge(m merger) merger {
return v.Merge(m.(*Variable))
}
// Required tests whether a variable is required or not.
func (v *Variable) Required() bool {
return v.Default == nil
}

View File

@ -278,6 +278,11 @@ func variablesStr(vs []*Variable) string {
required = " (required)" required = " (required)"
} }
declaredType := ""
if v.DeclaredType != "" {
declaredType = fmt.Sprintf(" (%s)", v.DeclaredType)
}
if v.Default == nil || v.Default == "" { if v.Default == nil || v.Default == "" {
v.Default = "<>" v.Default = "<>"
} }
@ -286,9 +291,10 @@ func variablesStr(vs []*Variable) string {
} }
result += fmt.Sprintf( result += fmt.Sprintf(
"%s%s\n %v\n %s\n", "%s%s%s\n %v\n %s\n",
k, k,
required, required,
declaredType,
v.Default, v.Default,
v.Description) v.Description)
} }

View File

@ -28,9 +28,10 @@ func (t *hclConfigurable) Config() (*Config, error) {
} }
type hclVariable struct { type hclVariable struct {
Default interface{} Default interface{}
Description string Description string
Fields []string `hcl:",decodedFields"` DeclaredType string `hcl:"type"`
Fields []string `hcl:",decodedFields"`
} }
var rawConfig struct { var rawConfig struct {
@ -70,9 +71,14 @@ func (t *hclConfigurable) Config() (*Config, error) {
} }
newVar := &Variable{ newVar := &Variable{
Name: k, Name: k,
Default: v.Default, DeclaredType: v.DeclaredType,
Description: v.Description, Default: v.Default,
Description: v.Description,
}
if err := newVar.ValidateTypeAndDefault(); err != nil {
return nil, err
} }
config.Variables = append(config.Variables, newVar) config.Variables = append(config.Variables, newVar)

View File

@ -444,6 +444,30 @@ func TestLoadDir_override(t *testing.T) {
} }
} }
func TestLoadFile_mismatchedVariableTypes(t *testing.T) {
_, err := LoadFile(filepath.Join(fixtureDir, "variable-mismatched-type.tf"))
if err == nil {
t.Fatalf("bad: expected error")
}
errorStr := err.Error()
if !strings.Contains(errorStr, "'not_a_map' has a default value which is not of type 'string'") {
t.Fatalf("bad: expected error has wrong text: %s", errorStr)
}
}
func TestLoadFile_badVariableTypes(t *testing.T) {
_, err := LoadFile(filepath.Join(fixtureDir, "bad-variable-type.tf"))
if err == nil {
t.Fatalf("bad: expected error")
}
errorStr := err.Error()
if !strings.Contains(errorStr, "'bad_type' must be of type string") {
t.Fatalf("bad: expected error has wrong text: %s", errorStr)
}
}
func TestLoadFile_provisioners(t *testing.T) { func TestLoadFile_provisioners(t *testing.T) {
c, err := LoadFile(filepath.Join(fixtureDir, "provisioners.tf")) c, err := LoadFile(filepath.Join(fixtureDir, "provisioners.tf"))
if err != nil { if err != nil {
@ -802,6 +826,12 @@ aws_security_group[firewall] (x5)
` `
const basicVariablesStr = ` const basicVariablesStr = `
bar (required) (string)
<>
<>
baz (map)
map[key:value]
<>
foo foo
bar bar
bar bar

View File

@ -0,0 +1,3 @@
variable "bad_type" {
type = "notatype"
}

View File

@ -3,6 +3,18 @@ variable "foo" {
description = "bar" description = "bar"
} }
variable "bar" {
type = "string"
}
variable "baz" {
type = "map"
default = {
key = "value"
}
}
provider "aws" { provider "aws" {
access_key = "foo" access_key = "foo"
secret_key = "bar" secret_key = "bar"

View File

@ -3,6 +3,15 @@
"foo": { "foo": {
"default": "bar", "default": "bar",
"description": "bar" "description": "bar"
},
"bar": {
"type": "string"
},
"baz": {
"type": "map",
"default": {
"key": "value"
}
} }
}, },

View File

@ -0,0 +1,7 @@
variable "not_a_map" {
type = "string"
default = {
i_am_not = "a string"
}
}

View File

@ -25,6 +25,21 @@ func TestContext2Validate_badVar(t *testing.T) {
} }
} }
func TestContext2Validate_varNoDefaultExplicitType(t *testing.T) {
m := testModule(t, "validate-var-no-default-explicit-type")
c := testContext2(t, &ContextOpts{
Module: m,
})
w, e := c.Validate()
if len(w) > 0 {
t.Fatalf("bad: %#v", w)
}
if len(e) == 0 {
t.Fatalf("bad: %#v", e)
}
}
func TestContext2Validate_computedVar(t *testing.T) { func TestContext2Validate_computedVar(t *testing.T) {
p := testProvider("aws") p := testProvider("aws")
m := testModule(t, "validate-computed-var") m := testModule(t, "validate-computed-var")

View File

@ -0,0 +1,5 @@
variable "maybe_a_map" {
type = "map"
// No default
}

View File

@ -23,9 +23,13 @@ already.
A variable configuration looks like the following: A variable configuration looks like the following:
``` ```
variable "key" {} variable "key" {
type = "string"
}
variable "images" { variable "images" {
type = "map"
default = { default = {
us-east-1 = "image-1234" us-east-1 = "image-1234"
us-west-2 = "image-4567" us-west-2 = "image-4567"
@ -39,13 +43,22 @@ The `variable` block configures a single input variable for
a Terraform configuration. Multiple variables blocks can be used to a Terraform configuration. Multiple variables blocks can be used to
add multiple variables. add multiple variables.
The `NAME` given to the variable block is the name used to The `name` given to the variable block is the name used to
set the variable via the CLI as well as reference the variable set the variable via the CLI as well as reference the variable
throughout the Terraform configuration. throughout the Terraform configuration.
Within the block (the `{ }`) is configuration for the variable. Within the block (the `{ }`) is configuration for the variable.
These are the parameters that can be set: These are the parameters that can be set:
* `type` (optional) - If set this defines the type of the variable.
Valid values are `string` and `map`. In older versions of Terraform
this parameter did not exist, and the type was inferred from the
default value, defaulting to `string` if no default was set. If a
type is not specified, the previous behavior is maintained. It is
recommended to set variable types explicitly in preference to relying
on inferrence - this allows variables of type `map` to be set in the
`terraform.tfvars` file without requiring a default value to be set.
* `default` (optional) - If set, this sets a default value * `default` (optional) - If set, this sets a default value
for the variable. If this isn't set, the variable is required for the variable. If this isn't set, the variable is required
and Terraform will error if not set. The default value can be and Terraform will error if not set. The default value can be
@ -59,15 +72,18 @@ These are the parameters that can be set:
------ ------
**Default values** can be either strings or maps. If a default **Default values** can be either strings or maps, and if specified
value is omitted and the variable is required, the value assigned must match the declared type of the variable. If no value is supplied
via the CLI must be a string. for a variable of type `map`, the values must be supplied in a
`terraform.tfvars` file - they cannot be input via the console.
String values are simple and represent a basic key to value String values are simple and represent a basic key to value
mapping where the key is the variable name. An example is: mapping where the key is the variable name. An example is:
``` ```
variable "key" { variable "key" {
type = "string"
default = "value" default = "value"
} }
``` ```
@ -79,6 +95,8 @@ An example:
``` ```
variable "images" { variable "images" {
type = "map"
default = { default = {
us-east-1 = "image-1234" us-east-1 = "image-1234"
us-west-2 = "image-4567" us-west-2 = "image-4567"
@ -115,6 +133,7 @@ The full syntax is:
``` ```
variable NAME { variable NAME {
[type = TYPE]
[default = DEFAULT] [default = DEFAULT]
[description = DESCRIPTION] [description = DESCRIPTION]
} }