Merge pull request #4795 from hashicorp/f-variable-types
core: Support explicit variable type declaration
This commit is contained in:
commit
327f11460c
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
variable "bad_type" {
|
||||||
|
type = "notatype"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
"foo": {
|
"foo": {
|
||||||
"default": "bar",
|
"default": "bar",
|
||||||
"description": "bar"
|
"description": "bar"
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"baz": {
|
||||||
|
"type": "map",
|
||||||
|
"default": {
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
variable "not_a_map" {
|
||||||
|
type = "string"
|
||||||
|
|
||||||
|
default = {
|
||||||
|
i_am_not = "a string"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
variable "maybe_a_map" {
|
||||||
|
type = "map"
|
||||||
|
|
||||||
|
// No default
|
||||||
|
}
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue