terraform/helper/schema/schema_test.go

5481 lines
106 KiB
Go
Raw Normal View History

2014-08-15 04:55:47 +02:00
package schema
import (
"bytes"
"errors"
"fmt"
"os"
2014-08-15 04:55:47 +02:00
"reflect"
"sort"
"strconv"
"strings"
2014-08-15 04:55:47 +02:00
"testing"
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
"github.com/hashicorp/hil"
2016-01-31 08:47:49 +01:00
"github.com/hashicorp/hil/ast"
2014-08-15 04:55:47 +02:00
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/hashcode"
2014-08-15 04:55:47 +02:00
"github.com/hashicorp/terraform/terraform"
)
func TestEnvDefaultFunc(t *testing.T) {
key := "TF_TEST_ENV_DEFAULT_FUNC"
defer os.Unsetenv(key)
f := EnvDefaultFunc(key, "42")
if err := os.Setenv(key, "foo"); err != nil {
t.Fatalf("err: %s", err)
}
actual, err := f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "foo" {
t.Fatalf("bad: %#v", actual)
}
if err := os.Unsetenv(key); err != nil {
t.Fatalf("err: %s", err)
}
actual, err = f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "42" {
t.Fatalf("bad: %#v", actual)
}
}
func TestMultiEnvDefaultFunc(t *testing.T) {
keys := []string{
"TF_TEST_MULTI_ENV_DEFAULT_FUNC1",
"TF_TEST_MULTI_ENV_DEFAULT_FUNC2",
}
defer func() {
for _, k := range keys {
os.Unsetenv(k)
}
}()
// Test that the first key is returned first
f := MultiEnvDefaultFunc(keys, "42")
if err := os.Setenv(keys[0], "foo"); err != nil {
t.Fatalf("err: %s", err)
}
actual, err := f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "foo" {
t.Fatalf("bad: %#v", actual)
}
if err := os.Unsetenv(keys[0]); err != nil {
t.Fatalf("err: %s", err)
}
// Test that the second key is returned if the first one is empty
f = MultiEnvDefaultFunc(keys, "42")
if err := os.Setenv(keys[1], "foo"); err != nil {
t.Fatalf("err: %s", err)
}
actual, err = f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "foo" {
t.Fatalf("bad: %#v", actual)
}
if err := os.Unsetenv(keys[1]); err != nil {
t.Fatalf("err: %s", err)
}
// Test that the default value is returned when no keys are set
actual, err = f()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "42" {
t.Fatalf("bad: %#v", actual)
}
}
2015-01-09 03:02:19 +01:00
func TestValueType_Zero(t *testing.T) {
cases := []struct {
Type ValueType
Value interface{}
}{
{TypeBool, false},
{TypeInt, 0},
{TypeFloat, 0.0},
2015-01-09 03:02:19 +01:00
{TypeString, ""},
{TypeList, []interface{}{}},
{TypeMap, map[string]interface{}{}},
{TypeSet, new(Set)},
2015-01-09 03:02:19 +01:00
}
for i, tc := range cases {
actual := tc.Type.Zero()
if !reflect.DeepEqual(actual, tc.Value) {
t.Fatalf("%d: %#v != %#v", i, actual, tc.Value)
}
}
}
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
func interfaceToVariableSwallowError(input interface{}) ast.Variable {
variable, _ := hil.InterfaceToVariable(input)
return variable
}
2014-08-15 04:55:47 +02:00
func TestSchemaMap_Diff(t *testing.T) {
cases := []struct {
Name string
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables map[string]ast.Variable
CustomizeDiff CustomizeDiffFunc
2014-09-18 01:33:24 +02:00
Diff *terraform.InstanceDiff
Err bool
2014-08-15 04:55:47 +02:00
}{
{
2014-08-15 04:55:47 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-15 04:55:47 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
RequiresNew: true,
},
},
},
Err: false,
},
{
2014-08-15 04:55:47 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-15 04:55:47 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
RequiresNew: true,
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: &terraform.InstanceState{
ID: "foo",
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Computed, but set in config",
2014-10-21 08:52:22 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: map[string]interface{}{
"availability_zone": "bar",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
},
},
},
Err: false,
},
{
Name: "Default",
2014-09-10 06:17:29 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Default: "foo",
},
},
State: nil,
Config: nil,
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-09-10 06:17:29 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
},
},
Err: false,
},
{
Name: "DefaultFunc, value",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
DefaultFunc: func() (interface{}, error) {
return "foo", nil
},
},
},
State: nil,
Config: nil,
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
},
},
Err: false,
},
{
Name: "DefaultFunc, configuration set",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
DefaultFunc: func() (interface{}, error) {
return "foo", nil
},
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "bar",
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
Err: false,
},
{
Name: "String with StateFunc",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
StateFunc: func(a interface{}) string {
return a.(string) + "!"
},
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo!",
NewExtra: "foo",
},
},
},
Err: false,
},
{
Name: "StateFunc not called with nil value",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
StateFunc: func(a interface{}) string {
t.Fatalf("should not get here!")
return ""
},
},
},
State: nil,
Config: map[string]interface{}{},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Variable (just checking)",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "${var.foo}",
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError("bar"),
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
Err: false,
},
{
Name: "Variable computed",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "${var.foo}",
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "${var.foo}",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Int decode",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"port": 27,
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"port": &terraform.ResourceAttrDiff{
Old: "",
New: "27",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "bool decode",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"port": false,
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"port": &terraform.ResourceAttrDiff{
Old: "",
helper/schema: Normalize bools to "true"/"false" in diffs For a long time now, the diff logic has relied on the behavior of `mapstructure.WeakDecode` to determine how various primitives are converted into strings. The `schema.DiffString` function is used for all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString. The `mapstructure` library's string representation of booleans is "0" and "1", which differs from `strconv.FormatBool`'s "false" and "true" (which is used in writing out boolean fields to the state). Because of this difference, diffs have long had the potential for cosmetically odd but semantically neutral output like: "true" => "1" "false" => "0" So long as `mapstructure.Decode` or `strconv.ParseBool` are used to interpret these strings, there's no functional problem. We had our first clear functional problem with #6005 and friends, where users noticed diffs like the above showing up unexpectedly and causing troubles when `ignore_changes` was in play. This particular bug occurs down in Terraform core's EvalIgnoreChanges. There, the diff is modified to account for ignored attributes, and special logic attempts to handle properly the situation where the ignored attribute was going to trigger a resource replacement. That logic relies on the string representations of the Old and New fields in the diff to be the same so that it filters properly. So therefore, we now get a bug when a diff includes `Old: "0", New: "false"` since the strings do not match, and `ignore_changes` is not properly handled. Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`. I spiked out a full `diffBool` function, but figuring out which pieces of `diffString` to duplicate there got hairy. This seemed like a simpler and more direct solution. Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
New: "false",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "Bool",
Schema: map[string]*Schema{
"delete": &Schema{
Type: TypeBool,
Optional: true,
Default: false,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"delete": "false",
},
},
Config: nil,
Diff: nil,
Err: false,
},
{
Name: "List decode",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, 2, 5},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "List decode with promotion",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
},
},
State: nil,
Config: map[string]interface{}{
"ports": "5",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "List decode with promotion with list",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{"5"},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, "${var.foo}"},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, "${var.foo}"},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
2014-10-10 23:50:35 +02:00
Old: "0",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.0": "1",
"ports.1": "2",
"ports.2": "5",
},
},
Config: map[string]interface{}{
"ports": []interface{}{1, 2, 5},
},
Diff: nil,
Err: false,
},
{
Name: "",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "2",
"ports.0": "1",
"ports.1": "2",
},
},
Config: map[string]interface{}{
"ports": []interface{}{1, 2, 5},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "3",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
2014-08-15 08:32:20 +02:00
{
Name: "",
2014-08-15 08:32:20 +02:00
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, 2, 5},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-15 08:32:20 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
2014-08-15 08:32:20 +02:00
New: "3",
RequiresNew: true,
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
RequiresNew: true,
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
RequiresNew: true,
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: map[string]interface{}{},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "List with computed set",
Schema: map[string]*Schema{
"config": &Schema{
Type: TypeList,
Optional: true,
ForceNew: true,
MinItems: 1,
Elem: &Resource{
Schema: map[string]*Schema{
"name": {
Type: TypeString,
Required: true,
},
"rules": {
Type: TypeSet,
Computed: true,
Elem: &Schema{Type: TypeString},
Set: HashString,
},
},
},
},
},
State: nil,
Config: map[string]interface{}{
"config": []interface{}{
map[string]interface{}{
"name": "hello",
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
RequiresNew: true,
},
"config.0.name": &terraform.ResourceAttrDiff{
Old: "",
New: "hello",
},
"config.0.rules.#": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 1},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Computed: true,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "0",
},
},
Config: nil,
Diff: nil,
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{"${var.foo}", 1},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{"2", "5"}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, "${var.foo}"},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError([]interface{}{
config.UnknownVariableValue, "5"}),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
2014-10-10 23:50:35 +02:00
New: "",
NewComputed: true,
},
},
},
Err: false,
},
2014-08-15 19:39:40 +02:00
{
Name: "Set",
2014-08-21 06:02:42 +02:00
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
2014-08-21 06:02:42 +02:00
Attributes: map[string]string{
"ports.#": "2",
"ports.1": "1",
"ports.2": "2",
2014-08-21 06:02:42 +02:00
},
},
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 1},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-21 06:02:42 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "1",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "2",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
2014-08-21 06:02:42 +02:00
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "Set",
2014-08-21 06:02:42 +02:00
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
2014-08-21 06:02:42 +02:00
Attributes: map[string]string{
"ports.#": "2",
"ports.1": "1",
"ports.2": "2",
2014-08-21 06:02:42 +02:00
},
},
Config: map[string]interface{}{},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-21 06:02:42 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "0",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "1",
New: "0",
NewRemoved: true,
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "2",
New: "0",
NewRemoved: true,
},
2014-08-21 06:02:42 +02:00
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "bar",
"ports.#": "1",
"ports.80": "80",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeSet,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeInt},
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
ps := m["ports"].([]interface{})
result := 0
for _, p := range ps {
result += p.(int)
}
return result
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ingress.#": "2",
"ingress.80.ports.#": "1",
"ingress.80.ports.0": "80",
"ingress.443.ports.#": "1",
"ingress.443.ports.0": "443",
},
},
Config: map[string]interface{}{
"ingress": []map[string]interface{}{
map[string]interface{}{
"ports": []interface{}{443},
},
map[string]interface{}{
"ports": []interface{}{80},
},
},
},
Diff: nil,
Err: false,
},
{
Name: "List of structure decode",
2014-08-15 19:39:40 +02:00
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
State: nil,
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{
"from": 8080,
2014-08-15 19:39:40 +02:00
},
},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ingress.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ingress.0.from": &terraform.ResourceAttrDiff{
Old: "",
New: "8080",
},
2014-08-15 19:39:40 +02:00
},
},
Err: false,
2014-08-15 19:39:40 +02:00
},
{
Name: "ComputedWhen",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
ComputedWhen: []string{"port"},
},
"port": &Schema{
Type: TypeInt,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
"port": "80",
},
},
Config: map[string]interface{}{
"port": 80,
},
Diff: nil,
Err: false,
},
{
Name: "",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
ComputedWhen: []string{"port"},
},
"port": &Schema{
Type: TypeInt,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"port": "80",
},
},
Config: map[string]interface{}{
"port": 80,
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
NewComputed: true,
},
},
},
Err: false,
},
/* TODO
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
ComputedWhen: []string{"port"},
},
"port": &Schema{
Type: TypeInt,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
"port": "80",
},
},
Config: map[string]interface{}{
"port": 8080,
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "foo",
NewComputed: true,
},
"port": &terraform.ResourceAttrDiff{
Old: "80",
New: "8080",
},
},
},
Err: false,
},
*/
2014-08-19 00:07:09 +02:00
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeMap,
},
},
State: nil,
Config: map[string]interface{}{
"config_vars": []interface{}{
map[string]interface{}{
"bar": "baz",
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"config_vars.%": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"config_vars.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Err: false,
},
{
Name: "Maps",
2014-10-09 03:25:31 +02:00
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeMap,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"config_vars.foo": "bar",
},
},
Config: map[string]interface{}{
"config_vars": []interface{}{
map[string]interface{}{
"bar": "baz",
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config_vars.foo": &terraform.ResourceAttrDiff{
Old: "bar",
NewRemoved: true,
},
"config_vars.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Err: false,
},
{
Name: "Maps",
2014-10-21 08:52:22 +02:00
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"vars.foo": "bar",
},
},
Config: map[string]interface{}{
"vars": []interface{}{
map[string]interface{}{
"bar": "baz",
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"vars.foo": &terraform.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
},
"vars.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Err: false,
},
{
Name: "Maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"vars.foo": "bar",
},
},
Config: nil,
Diff: nil,
Err: false,
},
{
Name: "Maps",
2014-08-19 00:07:09 +02:00
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
State: &terraform.InstanceState{
2014-08-19 00:07:09 +02:00
Attributes: map[string]string{
"config_vars.#": "1",
"config_vars.0.foo": "bar",
},
},
Config: map[string]interface{}{
"config_vars": []interface{}{
map[string]interface{}{
"bar": "baz",
},
},
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
2014-08-19 00:07:09 +02:00
Attributes: map[string]*terraform.ResourceAttrDiff{
"config_vars.0.foo": &terraform.ResourceAttrDiff{
Old: "bar",
NewRemoved: true,
},
"config_vars.0.bar": &terraform.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Err: false,
},
{
Name: "Maps",
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"config_vars.#": "1",
"config_vars.0.foo": "bar",
"config_vars.0.bar": "baz",
},
},
Config: map[string]interface{}{},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config_vars.#": &terraform.ResourceAttrDiff{
Old: "1",
New: "0",
},
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"config_vars.0.%": &terraform.ResourceAttrDiff{
Old: "2",
New: "0",
},
"config_vars.0.foo": &terraform.ResourceAttrDiff{
Old: "bar",
NewRemoved: true,
},
"config_vars.0.bar": &terraform.ResourceAttrDiff{
Old: "baz",
NewRemoved: true,
},
},
},
Err: false,
},
{
Name: "ForceNews",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
ForceNew: true,
},
"address": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "bar",
"address": "foo",
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "bar",
New: "foo",
RequiresNew: true,
},
"address": &terraform.ResourceAttrDiff{
Old: "foo",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
ForceNew: true,
},
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"availability_zone": "bar",
"ports.#": "1",
"ports.80": "80",
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
2014-09-18 01:33:24 +02:00
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "bar",
New: "foo",
RequiresNew: true,
},
"ports.#": &terraform.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeString},
Optional: true,
Computed: true,
Set: func(v interface{}) int {
return len(v.(string))
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"instances.#": "0",
},
},
Config: map[string]interface{}{
"instances": []interface{}{"${var.foo}"},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"instances.#": &terraform.ResourceAttrDiff{
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"gateway": &Schema{
Type: TypeString,
Optional: true,
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return m["index"].(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"route": []map[string]interface{}{
map[string]interface{}{
"index": "1",
"gateway": "${var.foo}",
},
},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"route.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"route.~1.index": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"route.~1.gateway": &terraform.ResourceAttrDiff{
Old: "",
New: "${var.foo}",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Set",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"gateway": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return m["index"].(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"route": []map[string]interface{}{
map[string]interface{}{
"index": "1",
"gateway": []interface{}{
"${var.foo}",
},
},
},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"route.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"route.~1.index": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"route.~1.gateway.#": &terraform.ResourceAttrDiff{
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Computed maps",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
Computed: true,
},
},
State: nil,
Config: nil,
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"vars.%": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
2014-12-16 18:05:16 +01:00
{
Name: "Computed maps",
2014-12-16 18:05:16 +01:00
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"vars.%": "0",
2014-12-16 18:05:16 +01:00
},
},
Config: map[string]interface{}{
"vars": map[string]interface{}{
"bar": "${var.foo}",
},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
2014-12-16 18:05:16 +01:00
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"vars.%": &terraform.ResourceAttrDiff{
2014-12-16 18:05:16 +01:00
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
2014-12-17 00:56:40 +01:00
{
Name: " - Empty",
2014-12-17 00:56:40 +01:00
Schema: map[string]*Schema{},
State: &terraform.InstanceState{},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Float",
Schema: map[string]*Schema{
"some_threshold": &Schema{
Type: TypeFloat,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"some_threshold": "567.8",
},
},
Config: map[string]interface{}{
"some_threshold": 12.34,
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"some_threshold": &terraform.ResourceAttrDiff{
Old: "567.8",
New: "12.34",
},
},
},
Err: false,
},
{
Name: "https://github.com/hashicorp/terraform/issues/824",
Schema: map[string]*Schema{
"block_device": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"device_name": &Schema{
Type: TypeString,
Required: true,
},
"delete_on_termination": &Schema{
Type: TypeBool,
Optional: true,
Default: true,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
return hashcode.String(buf.String())
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"block_device.#": "2",
"block_device.616397234.delete_on_termination": "true",
"block_device.616397234.device_name": "/dev/sda1",
"block_device.2801811477.delete_on_termination": "true",
"block_device.2801811477.device_name": "/dev/sdx",
},
},
Config: map[string]interface{}{
"block_device": []map[string]interface{}{
map[string]interface{}{
"device_name": "/dev/sda1",
},
map[string]interface{}{
"device_name": "/dev/sdx",
},
},
},
Diff: nil,
Err: false,
},
{
Name: "Zero value in state shouldn't result in diff",
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeBool,
Optional: true,
ForceNew: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"port": "false",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
2015-02-17 20:12:45 +01:00
{
Name: "Same as prev, but for sets",
2015-02-17 20:12:45 +01:00
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"gateway": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return m["index"].(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"route.#": "0",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
2015-02-17 22:15:30 +01:00
{
Name: "A set computed element shouldn't cause a diff",
2015-02-17 22:15:30 +01:00
Schema: map[string]*Schema{
"active": &Schema{
Type: TypeBool,
Computed: true,
ForceNew: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"active": "true",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "An empty set should show up in the diff",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeString},
Optional: true,
ForceNew: true,
Set: func(v interface{}) int {
return len(v.(string))
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"instances.#": "1",
"instances.3": "foo",
},
},
Config: map[string]interface{}{},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"instances.#": &terraform.ResourceAttrDiff{
Old: "1",
New: "0",
RequiresNew: true,
},
"instances.3": &terraform.ResourceAttrDiff{
Old: "foo",
New: "",
NewRemoved: true,
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "Map with empty value",
Schema: map[string]*Schema{
"vars": &Schema{
Type: TypeMap,
},
},
State: nil,
Config: map[string]interface{}{
"vars": map[string]interface{}{
"foo": "",
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
core: Use .% instead of .# for maps in state The flatmapped representation of state prior to this commit encoded maps and lists (and therefore by extension, sets) with a key corresponding to the number of elements, or the unknown variable indicator under a .# key and then individual items. For example, the list ["a", "b", "c"] would have been encoded as: listname.# = 3 listname.0 = "a" listname.1 = "b" listname.2 = "c" And the map {"key1": "value1", "key2", "value2"} would have been encoded as: mapname.# = 2 mapname.key1 = "value1" mapname.key2 = "value2" Sets use the hash code as the key - for example a set with a (fictional) hashcode calculation may look like: setname.# = 2 setname.12312512 = "value1" setname.56345233 = "value2" Prior to the work done to extend the type system, this was sufficient since the internal representation of these was effectively the same. However, following the separation of maps and lists into distinct first-class types, this encoding presents a problem: given a state file, it is impossible to tell the encoding of an empty list and an empty map apart. This presents problems for the type checker during interpolation, as many interpolation functions will operate on only one of these two structures. This commit therefore changes the representation in state of maps to use a "%" as the key for the number of elements. Consequently the map above will now be encoded as: mapname.% = 2 mapname.key1 = "value1" mapname.key2 = "value2" This has the effect of an empty list (or set) now being encoded as: listname.# = 0 And an empty map now being encoded as: mapname.% = 0 Therefore we can eliminate some nasty guessing logic from the resource variable supplier for interpolation, at the cost of having to migrate state up front (to follow in a subsequent commit). In order to reduce the number of potential situations in which resources would be "forced new", we continue to accept "#" as the count key when reading maps via helper/schema. There is no situation under which we can allow "#" as an actual map key in any case, as it would not be distinguishable from a list or set in state.
2016-06-05 10:34:43 +02:00
"vars.%": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"vars.foo": &terraform.ResourceAttrDiff{
Old: "",
New: "",
},
},
},
Err: false,
},
{
Name: "Unset bool, not in state",
Schema: map[string]*Schema{
"force": &Schema{
Type: TypeBool,
Optional: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Unset set, not in state",
Schema: map[string]*Schema{
"metadata_keys": &Schema{
Type: TypeSet,
Optional: true,
ForceNew: true,
Elem: &Schema{Type: TypeInt},
Set: func(interface{}) int { return 0 },
},
},
State: nil,
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Unset list in state, should not show up computed",
Schema: map[string]*Schema{
"metadata_keys": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &Schema{Type: TypeInt},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"metadata_keys.#": "0",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
{
Name: "Set element computed substring",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{1, "${var.foo}32"},
},
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-22 02:03:24 +02:00
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "Computed map without config that's known to be empty does not generate diff",
Schema: map[string]*Schema{
"tags": &Schema{
Type: TypeMap,
Computed: true,
},
},
Config: nil,
State: &terraform.InstanceState{
Attributes: map[string]string{
"tags.%": "0",
},
},
Diff: nil,
Err: false,
},
{
Name: "Set with hyphen keys",
Schema: map[string]*Schema{
"route": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{
Type: TypeInt,
Required: true,
},
"gateway-name": &Schema{
Type: TypeString,
Optional: true,
},
},
},
Set: func(v interface{}) int {
m := v.(map[string]interface{})
return m["index"].(int)
},
},
},
State: nil,
Config: map[string]interface{}{
"route": []map[string]interface{}{
map[string]interface{}{
"index": "1",
"gateway-name": "hello",
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"route.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"route.1.index": &terraform.ResourceAttrDiff{
Old: "",
New: "1",
},
"route.1.gateway-name": &terraform.ResourceAttrDiff{
Old: "",
New: "hello",
},
},
},
Err: false,
},
{
Name: ": StateFunc in nested set (#1759)",
Schema: map[string]*Schema{
"service_account": &Schema{
Type: TypeList,
Optional: true,
ForceNew: true,
Elem: &Resource{
Schema: map[string]*Schema{
"scopes": &Schema{
Type: TypeSet,
Required: true,
ForceNew: true,
Elem: &Schema{
Type: TypeString,
StateFunc: func(v interface{}) string {
return v.(string) + "!"
},
},
Set: func(v interface{}) int {
i, err := strconv.Atoi(v.(string))
if err != nil {
t.Fatalf("err: %s", err)
}
return i
},
},
},
},
},
},
State: nil,
Config: map[string]interface{}{
"service_account": []map[string]interface{}{
{
"scopes": []interface{}{"123"},
},
},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"service_account.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
RequiresNew: true,
},
"service_account.0.scopes.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
RequiresNew: true,
},
"service_account.0.scopes.123": &terraform.ResourceAttrDiff{
Old: "",
New: "123!",
NewExtra: "123",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "Removing set elements",
Schema: map[string]*Schema{
"instances": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeString},
Optional: true,
ForceNew: true,
Set: func(v interface{}) int {
return len(v.(string))
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"instances.#": "2",
"instances.3": "333",
"instances.2": "22",
},
},
Config: map[string]interface{}{
"instances": []interface{}{"333", "4444"},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"instances.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "2",
},
"instances.2": &terraform.ResourceAttrDiff{
Old: "22",
New: "",
NewRemoved: true,
RequiresNew: true,
},
"instances.3": &terraform.ResourceAttrDiff{
Old: "333",
New: "333",
},
"instances.4": &terraform.ResourceAttrDiff{
Old: "",
New: "4444",
RequiresNew: true,
},
},
},
Err: false,
},
helper/schema: Normalize bools to "true"/"false" in diffs For a long time now, the diff logic has relied on the behavior of `mapstructure.WeakDecode` to determine how various primitives are converted into strings. The `schema.DiffString` function is used for all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString. The `mapstructure` library's string representation of booleans is "0" and "1", which differs from `strconv.FormatBool`'s "false" and "true" (which is used in writing out boolean fields to the state). Because of this difference, diffs have long had the potential for cosmetically odd but semantically neutral output like: "true" => "1" "false" => "0" So long as `mapstructure.Decode` or `strconv.ParseBool` are used to interpret these strings, there's no functional problem. We had our first clear functional problem with #6005 and friends, where users noticed diffs like the above showing up unexpectedly and causing troubles when `ignore_changes` was in play. This particular bug occurs down in Terraform core's EvalIgnoreChanges. There, the diff is modified to account for ignored attributes, and special logic attempts to handle properly the situation where the ignored attribute was going to trigger a resource replacement. That logic relies on the string representations of the Old and New fields in the diff to be the same so that it filters properly. So therefore, we now get a bug when a diff includes `Old: "0", New: "false"` since the strings do not match, and `ignore_changes` is not properly handled. Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`. I spiked out a full `diffBool` function, but figuring out which pieces of `diffString` to duplicate there got hairy. This seemed like a simpler and more direct solution. Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
{
Name: "Bools can be set with 0/1 in config, still get true/false",
helper/schema: Normalize bools to "true"/"false" in diffs For a long time now, the diff logic has relied on the behavior of `mapstructure.WeakDecode` to determine how various primitives are converted into strings. The `schema.DiffString` function is used for all primitive field types: TypeBool, TypeInt, TypeFloat, and TypeString. The `mapstructure` library's string representation of booleans is "0" and "1", which differs from `strconv.FormatBool`'s "false" and "true" (which is used in writing out boolean fields to the state). Because of this difference, diffs have long had the potential for cosmetically odd but semantically neutral output like: "true" => "1" "false" => "0" So long as `mapstructure.Decode` or `strconv.ParseBool` are used to interpret these strings, there's no functional problem. We had our first clear functional problem with #6005 and friends, where users noticed diffs like the above showing up unexpectedly and causing troubles when `ignore_changes` was in play. This particular bug occurs down in Terraform core's EvalIgnoreChanges. There, the diff is modified to account for ignored attributes, and special logic attempts to handle properly the situation where the ignored attribute was going to trigger a resource replacement. That logic relies on the string representations of the Old and New fields in the diff to be the same so that it filters properly. So therefore, we now get a bug when a diff includes `Old: "0", New: "false"` since the strings do not match, and `ignore_changes` is not properly handled. Here, we introduce `TypeBool`-specific normalizing into `finalizeDiff`. I spiked out a full `diffBool` function, but figuring out which pieces of `diffString` to duplicate there got hairy. This seemed like a simpler and more direct solution. Fixes #6005 (and potentially others!)
2016-05-05 16:00:58 +02:00
Schema: map[string]*Schema{
"one": &Schema{
Type: TypeBool,
Optional: true,
},
"two": &Schema{
Type: TypeBool,
Optional: true,
},
"three": &Schema{
Type: TypeBool,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"one": "false",
"two": "true",
"three": "true",
},
},
Config: map[string]interface{}{
"one": "1",
"two": "0",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"one": &terraform.ResourceAttrDiff{
Old: "false",
New: "true",
},
"two": &terraform.ResourceAttrDiff{
Old: "true",
New: "false",
},
"three": &terraform.ResourceAttrDiff{
Old: "true",
New: "false",
NewRemoved: true,
},
},
},
Err: false,
},
{
Name: "tainted in state w/ no attr changes is still a replacement",
Schema: map[string]*Schema{},
State: &terraform.InstanceState{
Attributes: map[string]string{
"id": "someid",
},
Tainted: true,
},
Config: map[string]interface{}{},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
DestroyTainted: true,
},
Err: false,
},
{
Name: "Set ForceNew only marks the changing element as ForceNew",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
ForceNew: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.1": "1",
"ports.2": "2",
"ports.4": "4",
},
},
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 1},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "3",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "1",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "2",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
RequiresNew: true,
},
"ports.4": &terraform.ResourceAttrDiff{
Old: "4",
New: "0",
NewRemoved: true,
RequiresNew: true,
},
},
},
},
{
Name: "removed optional items should trigger ForceNew",
Schema: map[string]*Schema{
"description": &Schema{
Type: TypeString,
ForceNew: true,
Optional: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"description": "foo",
},
},
Config: map[string]interface{}{},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"description": &terraform.ResourceAttrDiff{
Old: "foo",
New: "",
RequiresNew: true,
NewRemoved: true,
},
},
},
Err: false,
},
// GH-7715
{
Name: "computed value for boolean field",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeBool,
ForceNew: true,
Computed: true,
Optional: true,
},
},
State: &terraform.InstanceState{},
Config: map[string]interface{}{
"foo": "${var.foo}",
},
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(
config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "",
New: "false",
NewComputed: true,
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "Set ForceNew marks count as ForceNew if computed",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
ForceNew: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.1": "1",
"ports.2": "2",
"ports.4": "4",
},
},
Config: map[string]interface{}{
"ports": []interface{}{"${var.foo}", 2, 1},
},
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "3",
New: "",
NewComputed: true,
RequiresNew: true,
},
},
},
},
{
Name: "List with computed schema and ForceNew",
Schema: map[string]*Schema{
"config": &Schema{
Type: TypeList,
Optional: true,
ForceNew: true,
Elem: &Schema{
Type: TypeString,
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"config.#": "2",
"config.0": "a",
"config.1": "b",
},
},
Config: map[string]interface{}{
"config": []interface{}{"${var.a}", "${var.b}"},
},
ConfigVariables: map[string]ast.Variable{
"var.a": interfaceToVariableSwallowError(
config.UnknownVariableValue),
"var.b": interfaceToVariableSwallowError(
config.UnknownVariableValue),
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "",
RequiresNew: true,
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "overridden diff with a CustomizeDiff function, ForceNew not in schema",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("availability_zone", "bar"); err != nil {
return err
}
if err := d.ForceNew("availability_zone"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
Err: false,
},
{
helper/schema: ResourceDiff ForceNew attribute correctness A couple of bugs have been discovered in ResourceDiff.ForceNew: * NewRemoved is not preserved when a diff for a key is already present. This is because the second diff that happens after customization performs a second getChange on not just state and config, but also on the pre-existing diff. This results in Exists == true, meaning nil is never returned as a new value. * ForceNew was doing the work of adding the key to the list of changed keys by doing a full SetNew on the existing value. This has a side effect of fetching zero values from what were otherwise undefined values and creating diffs for these values where there should not have been (example: "" => "0"). This update fixes these scenarios by: * Adding a new private function to check the existing diff for NewRemoved keys. This is included in the check on new values in diffChange. * Keys that have been flagged as ForceNew (or parent keys of lists and sets that have been flagged as ForceNew) are now maintained in a separate map. UpdatedKeys now returns the results of both of these maps, but otherwise these keys are ignored by ResourceDiff. * Pursuant the above, values are no longer pushed into the newDiff writer by ForceNew. This prevents the zero value problem, and makes for a cleaner implementation where the provider has to "manually" SetNew to update the appropriate values in the writer. It also prevents non-computed keys from winding up in the diff, which ResourceDiff normally blocks by design. There are also a couple of tests for cases that should never come up right now involving Optional/Computed values and NewRemoved, for which explanations are given in annotations of each test. These are here to guard against future regressions.
2018-04-07 22:06:47 +02:00
// NOTE: This case is technically impossible in the current
// implementation, because optional+computed values never show up in the
// diff. In the event behavior changes this test should ensure that the
// intended diff still shows up.
Name: "overridden removed attribute diff with a CustomizeDiff function, ForceNew not in schema",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: nil,
Config: map[string]interface{}{},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("availability_zone", "bar"); err != nil {
return err
}
if err := d.ForceNew("availability_zone"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "overridden diff with a CustomizeDiff function, ForceNew in schema",
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("availability_zone", "bar"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
RequiresNew: true,
},
},
},
Err: false,
},
{
Name: "required field with computed diff added with CustomizeDiff function",
Schema: map[string]*Schema{
"ami_id": &Schema{
Type: TypeString,
Required: true,
},
"instance_id": &Schema{
Type: TypeString,
Computed: true,
},
},
State: nil,
Config: map[string]interface{}{
"ami_id": "foo",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("instance_id", "bar"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ami_id": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"instance_id": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
},
Err: false,
},
{
Name: "Set ForceNew only marks the changing element as ForceNew - CustomizeDiffFunc edition",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"ports.#": "3",
"ports.1": "1",
"ports.2": "2",
"ports.4": "4",
},
},
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 6},
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if err := d.SetNew("ports", []interface{}{5, 2, 1}); err != nil {
return err
}
if err := d.ForceNew("ports"); err != nil {
return err
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "3",
New: "3",
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "1",
New: "1",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "2",
New: "2",
},
"ports.5": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
RequiresNew: true,
},
"ports.4": &terraform.ResourceAttrDiff{
Old: "4",
New: "0",
NewRemoved: true,
RequiresNew: true,
},
},
},
},
{
Name: "tainted resource does not run CustomizeDiffFunc",
Schema: map[string]*Schema{},
State: &terraform.InstanceState{
Attributes: map[string]string{
"id": "someid",
},
Tainted: true,
},
Config: map[string]interface{}{},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
return errors.New("diff customization should not have run")
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{},
DestroyTainted: true,
},
Err: false,
},
{
Name: "NewComputed based on a conditional with CustomizeDiffFunc",
Schema: map[string]*Schema{
"etag": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"version_id": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"etag": "foo",
"version_id": "1",
},
},
Config: map[string]interface{}{
"etag": "bar",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
if d.HasChange("etag") {
d.SetNewComputed("version_id")
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"etag": &terraform.ResourceAttrDiff{
Old: "foo",
New: "bar",
},
"version_id": &terraform.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
},
},
},
Err: false,
},
{
Name: "vetoing a diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: map[string]interface{}{
"foo": "baz",
},
CustomizeDiff: func(d *ResourceDiff, meta interface{}) error {
return fmt.Errorf("diff vetoed")
},
Err: true,
},
// A lot of resources currently depended on using the empty string as a
// nil/unset value.
// FIXME: We want this to eventually produce a diff, since there
// technically is a new value in the config.
{
Name: "optional, computed, empty string",
Schema: map[string]*Schema{
"attr": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"attr": "bar",
},
},
// this does necessarily depend on an interpolated value, but this
// is often how it comes about in a configuration, otherwise the
// value would be unset.
Config: map[string]interface{}{
"attr": "${var.foo}",
},
ConfigVariables: map[string]ast.Variable{
"var.foo": interfaceToVariableSwallowError(""),
},
},
{
Name: "optional, computed, empty string should not crash in CustomizeDiff",
Schema: map[string]*Schema{
"unrelated_set": {
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeString},
},
"stream_enabled": {
Type: TypeBool,
Optional: true,
},
"stream_view_type": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.InstanceState{
Attributes: map[string]string{
"unrelated_set.#": "0",
"stream_enabled": "true",
"stream_view_type": "KEYS_ONLY",
},
},
Config: map[string]interface{}{
"stream_enabled": false,
"stream_view_type": "",
},
CustomizeDiff: func(diff *ResourceDiff, v interface{}) error {
v, ok := diff.GetOk("unrelated_set")
if ok {
return fmt.Errorf("Didn't expect unrelated_set: %#v", v)
}
return nil
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"stream_enabled": {
Old: "true",
New: "false",
},
},
},
},
2014-08-15 04:55:47 +02:00
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(tc.ConfigVariables) > 0 {
if err := c.Interpolate(tc.ConfigVariables); err != nil {
t.Fatalf("err: %s", err)
}
}
2014-08-15 04:55:47 +02:00
d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), tc.CustomizeDiff, nil)
if err != nil != tc.Err {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(tc.Diff, d) {
t.Fatalf("expected:\n%#v\n\ngot:\n%#v", tc.Diff, d)
}
})
2014-08-15 04:55:47 +02:00
}
}
2014-08-16 07:00:16 +02:00
2014-09-29 19:25:43 +02:00
func TestSchemaMap_Input(t *testing.T) {
cases := map[string]struct {
2014-09-29 19:25:43 +02:00
Schema map[string]*Schema
Config map[string]interface{}
Input map[string]string
Result map[string]interface{}
Err bool
}{
/*
* String decode
*/
"no input on optional field with no config": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
},
},
Input: map[string]string{},
Result: map[string]interface{}{},
Err: false,
},
"input ignored when config has a value": {
2014-09-29 19:25:43 +02:00
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{}{},
2014-09-29 19:25:43 +02:00
Err: false,
},
"input ignored when schema has a default": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Default: "foo",
Optional: true,
},
},
Input: map[string]string{
"availability_zone": "bar",
},
Result: map[string]interface{}{},
Err: false,
},
"input ignored when default function returns a value": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
DefaultFunc: func() (interface{}, error) {
return "foo", nil
},
Optional: true,
},
},
Input: map[string]string{
"availability_zone": "bar",
},
Result: map[string]interface{}{},
Err: false,
},
"input ignored when default function returns an empty string": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Default: "",
Optional: true,
},
},
Input: map[string]string{
"availability_zone": "bar",
},
Result: map[string]interface{}{},
Err: false,
},
"input used when default function returns nil": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
DefaultFunc: func() (interface{}, error) {
return nil, nil
},
Required: true,
},
},
Input: map[string]string{
"availability_zone": "bar",
},
Result: map[string]interface{}{
"availability_zone": "bar",
},
Err: false,
},
"input not used when optional default function returns nil": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
DefaultFunc: func() (interface{}, error) {
return nil, nil
},
Optional: true,
},
},
Input: map[string]string{},
Result: map[string]interface{}{},
Err: false,
},
2014-09-29 19:25:43 +02:00
}
for i, tc := range cases {
if tc.Config == nil {
tc.Config = make(map[string]interface{})
}
2014-09-29 19:25:43 +02:00
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
input := new(terraform.MockUIInput)
input.InputReturnMap = tc.Input
rc := terraform.NewResourceConfig(c)
rc.Config = make(map[string]interface{})
actual, err := schemaMap(tc.Schema).Input(input, rc)
2015-10-08 14:48:04 +02:00
if err != nil != tc.Err {
t.Fatalf("#%v err: %s", i, err)
2014-09-29 19:25:43 +02:00
}
if !reflect.DeepEqual(tc.Result, actual.Config) {
t.Fatalf("#%v: bad:\n\ngot: %#v\nexpected: %#v", i, actual.Config, tc.Result)
2014-09-29 19:25:43 +02:00
}
}
}
func TestSchemaMap_InputDefault(t *testing.T) {
emptyConfig := make(map[string]interface{})
c, err := config.NewRawConfig(emptyConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
rc := terraform.NewResourceConfig(c)
rc.Config = make(map[string]interface{})
input := new(terraform.MockUIInput)
input.InputFn = func(opts *terraform.InputOpts) (string, error) {
t.Fatalf("InputFn should not be called on: %#v", opts)
return "", nil
}
schema := map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Default: "foo",
Optional: true,
},
}
actual, err := schemaMap(schema).Input(input, rc)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := map[string]interface{}{}
if !reflect.DeepEqual(expected, actual.Config) {
t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected)
}
}
func TestSchemaMap_InputDeprecated(t *testing.T) {
emptyConfig := make(map[string]interface{})
c, err := config.NewRawConfig(emptyConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
rc := terraform.NewResourceConfig(c)
rc.Config = make(map[string]interface{})
input := new(terraform.MockUIInput)
input.InputFn = func(opts *terraform.InputOpts) (string, error) {
t.Fatalf("InputFn should not be called on: %#v", opts)
return "", nil
}
schema := map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Deprecated: "long gone",
Optional: true,
},
}
actual, err := schemaMap(schema).Input(input, rc)
if err != nil {
t.Fatalf("err: %s", err)
}
expected := map[string]interface{}{}
if !reflect.DeepEqual(expected, actual.Config) {
t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected)
}
}
func TestSchemaMap_InternalValidate(t *testing.T) {
cases := map[string]struct {
In map[string]*Schema
Err bool
}{
"nothing": {
nil,
false,
},
"Both optional and required": {
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Required: true,
},
},
true,
},
"No optional and no required": {
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
},
},
true,
},
"Missing Type": {
map[string]*Schema{
"foo": &Schema{
Required: true,
},
},
true,
},
"Required but computed": {
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Required: true,
Computed: true,
},
},
true,
},
"Looks good": {
map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
false,
},
"Computed but has default": {
2014-09-10 06:17:29 +02:00
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
Default: "foo",
},
},
true,
},
"Required but has default": {
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Required: true,
Default: "foo",
},
},
true,
},
"List element not set": {
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
},
},
true,
},
"List default": {
2014-09-10 06:17:29 +02:00
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
Default: "foo",
},
},
true,
},
"List element computed": {
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Elem: &Schema{
Type: TypeInt,
Computed: true,
},
},
},
true,
},
"List element with Set set": {
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
Set: func(interface{}) int { return 0 },
Optional: true,
},
},
true,
},
"Set element with no Set set": {
map[string]*Schema{
"foo": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Optional: true,
},
},
false,
},
"Required but computedWhen": {
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Required: true,
ComputedWhen: []string{"foo"},
},
},
true,
},
"Conflicting attributes cannot be required": {
map[string]*Schema{
"blacklist": &Schema{
Type: TypeBool,
Required: true,
},
"whitelist": &Schema{
Type: TypeBool,
Optional: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},
"Attribute with conflicts cannot be required": {
map[string]*Schema{
"whitelist": &Schema{
Type: TypeBool,
Required: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},
"ConflictsWith cannot be used w/ ComputedWhen": {
map[string]*Schema{
"blacklist": &Schema{
Type: TypeBool,
ComputedWhen: []string{"foor"},
},
"whitelist": &Schema{
Type: TypeBool,
Required: true,
ConflictsWith: []string{"blacklist"},
},
},
true,
},
"Sub-resource invalid": {
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"foo": new(Schema),
},
},
},
},
true,
},
"Sub-resource valid": {
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
},
},
},
false,
},
"ValidateFunc on non-primitive": {
map[string]*Schema{
"foo": &Schema{
Type: TypeSet,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
return
},
},
},
true,
},
"computed-only field with validateFunc": {
map[string]*Schema{
"string": &Schema{
Type: TypeString,
Computed: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
es = append(es, fmt.Errorf("this is not fine"))
return
},
},
},
true,
},
"computed-only field with diffSuppressFunc": {
map[string]*Schema{
"string": &Schema{
Type: TypeString,
Computed: true,
DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool {
// Always suppress any diff
return false
},
},
},
true,
},
"invalid field name format #1": {
map[string]*Schema{
"with space": &Schema{
Type: TypeString,
Optional: true,
},
},
true,
},
"invalid field name format #2": {
map[string]*Schema{
"WithCapitals": &Schema{
Type: TypeString,
Optional: true,
},
},
true,
},
"invalid field name format of a Deprecated field": {
map[string]*Schema{
"WithCapitals": &Schema{
Type: TypeString,
Optional: true,
Deprecated: "Use with_underscores instead",
},
},
false,
},
"invalid field name format of a Removed field": {
map[string]*Schema{
"WithCapitals": &Schema{
Type: TypeString,
Optional: true,
Removed: "Use with_underscores instead",
},
},
false,
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
err := schemaMap(tc.In).InternalValidate(nil)
if err != nil != tc.Err {
if tc.Err {
t.Fatalf("%q: Expected error did not occur:\n\n%#v", tn, tc.In)
}
t.Fatalf("%q: Unexpected error occurred: %s\n\n%#v", tn, err, tc.In)
}
})
}
}
2016-08-31 22:26:57 +02:00
func TestSchemaMap_DiffSuppress(t *testing.T) {
cases := map[string]struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
ConfigVariables map[string]ast.Variable
ExpectedDiff *terraform.InstanceDiff
Err bool
}{
"#0 - Suppress otherwise valid diff by returning true": {
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool {
// Always suppress any diff
return true
},
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
ExpectedDiff: nil,
Err: false,
},
2016-08-31 22:26:57 +02:00
"#1 - Don't suppress diff by returning false": {
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool {
// Always suppress any diff
return false
},
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
ExpectedDiff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Err: false,
},
"Default with suppress makes no diff": {
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Default: "foo",
DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool {
return true
},
},
},
State: nil,
Config: map[string]interface{}{},
ExpectedDiff: nil,
Err: false,
},
"Default with false suppress makes diff": {
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Default: "foo",
DiffSuppressFunc: func(k, old, new string, d *ResourceData) bool {
return false
},
},
},
State: nil,
Config: map[string]interface{}{},
ExpectedDiff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Err: false,
},
"Complex structure with set of computed string should mark root set as computed": {
Schema: map[string]*Schema{
"outer": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"outer_str": &Schema{
Type: TypeString,
Optional: true,
},
"inner": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"inner_str": &Schema{
Type: TypeString,
Optional: true,
},
},
},
Set: func(v interface{}) int {
return 2
},
},
},
},
Set: func(v interface{}) int {
return 1
},
},
},
State: nil,
Config: map[string]interface{}{
"outer": []map[string]interface{}{
map[string]interface{}{
"outer_str": "foo",
"inner": []map[string]interface{}{
map[string]interface{}{
"inner_str": "${var.bar}",
},
},
},
},
},
ConfigVariables: map[string]ast.Variable{
"var.bar": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
ExpectedDiff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"outer.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"outer.~1.outer_str": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"outer.~1.inner.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"outer.~1.inner.~2.inner_str": &terraform.ResourceAttrDiff{
Old: "",
New: "${var.bar}",
NewComputed: true,
},
},
},
Err: false,
},
"Complex structure with complex list of computed string should mark root set as computed": {
Schema: map[string]*Schema{
"outer": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"outer_str": &Schema{
Type: TypeString,
Optional: true,
},
"inner": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"inner_str": &Schema{
Type: TypeString,
Optional: true,
},
},
},
},
},
},
Set: func(v interface{}) int {
return 1
},
},
},
State: nil,
Config: map[string]interface{}{
"outer": []map[string]interface{}{
map[string]interface{}{
"outer_str": "foo",
"inner": []map[string]interface{}{
map[string]interface{}{
"inner_str": "${var.bar}",
},
},
},
},
},
ConfigVariables: map[string]ast.Variable{
"var.bar": interfaceToVariableSwallowError(config.UnknownVariableValue),
},
ExpectedDiff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"outer.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"outer.~1.outer_str": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
},
"outer.~1.inner.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"outer.~1.inner.0.inner_str": &terraform.ResourceAttrDiff{
Old: "",
New: "${var.bar}",
NewComputed: true,
},
},
},
Err: false,
},
2016-08-31 22:26:57 +02:00
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
2016-08-31 22:26:57 +02:00
t.Fatalf("#%q err: %s", tn, err)
}
if len(tc.ConfigVariables) > 0 {
if err := c.Interpolate(tc.ConfigVariables); err != nil {
t.Fatalf("#%q err: %s", tn, err)
}
}
2016-08-31 22:26:57 +02:00
d, err := schemaMap(tc.Schema).Diff(tc.State, terraform.NewResourceConfig(c), nil, nil)
if err != nil != tc.Err {
t.Fatalf("#%q err: %s", tn, err)
}
if !reflect.DeepEqual(tc.ExpectedDiff, d) {
t.Fatalf("#%q:\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.ExpectedDiff, d)
}
})
2016-08-31 22:26:57 +02:00
}
}
2014-08-16 07:00:16 +02:00
func TestSchemaMap_Validate(t *testing.T) {
cases := map[string]struct {
Schema map[string]*Schema
Config map[string]interface{}
Vars map[string]string
Err bool
Errors []error
Warnings []string
2014-08-16 07:00:16 +02:00
}{
"Good": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
},
"Good, because the var is not set and that error will come elsewhere": {
Schema: map[string]*Schema{
"size": &Schema{
Type: TypeInt,
Required: true,
},
},
Config: map[string]interface{}{
"size": "${var.foo}",
},
Vars: map[string]string{
"var.foo": config.UnknownVariableValue,
},
},
"Required field not set": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
},
},
Config: map[string]interface{}{},
Err: true,
},
"Invalid basic type": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Required: true,
},
},
Config: map[string]interface{}{
"port": "I am invalid",
},
Err: true,
},
"Invalid complex type": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeString,
Optional: true,
},
},
Config: map[string]interface{}{
"user_data": []interface{}{
map[string]interface{}{
"foo": "bar",
},
},
},
Err: true,
},
"Bad type, interpolated": {
Schema: map[string]*Schema{
"size": &Schema{
Type: TypeInt,
Required: true,
},
},
Config: map[string]interface{}{
"size": "${var.foo}",
},
Vars: map[string]string{
"var.foo": "nope",
},
Err: true,
},
"Required but has DefaultFunc": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
DefaultFunc: func() (interface{}, error) {
return "foo", nil
},
},
},
Config: nil,
},
"Required but has DefaultFunc return nil": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
DefaultFunc: func() (interface{}, error) {
return nil, nil
},
},
},
Config: nil,
Err: true,
},
"List with promotion": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
Optional: true,
},
},
Config: map[string]interface{}{
"ingress": "5",
},
Err: false,
},
"List with promotion set as list": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
Optional: true,
},
},
Config: map[string]interface{}{
"ingress": []interface{}{"5"},
},
Err: false,
},
"Optional sub-resource": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{},
Err: false,
},
"Sub-resource is the wrong type": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Required: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{"foo"},
},
Err: true,
},
"Not a list": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": "foo",
},
Err: true,
},
"Required sub-resource field": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{},
},
},
Err: true,
},
"Good sub-resource": {
2014-08-16 07:00:16 +02:00
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Optional: true,
2014-08-16 07:00:16 +02:00
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{
"from": 80,
},
},
},
Err: false,
},
"Good sub-resource, interpolated value": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
`${map("from", "80")}`,
},
},
Vars: map[string]string{},
Err: false,
},
"Good sub-resource, computed value": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"from": &Schema{
Type: TypeInt,
Optional: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
`${map("from", var.port)}`,
},
},
Vars: map[string]string{
"var.port": config.UnknownVariableValue,
},
2014-08-16 07:00:16 +02:00
Err: false,
},
"Invalid/unknown field": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Config: map[string]interface{}{
"foo": "bar",
},
Err: true,
},
"Invalid/unknown field with computed value": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Config: map[string]interface{}{
"foo": "${var.foo}",
},
Vars: map[string]string{
"var.foo": config.UnknownVariableValue,
},
Err: true,
},
"Computed field set": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
},
},
Config: map[string]interface{}{
"availability_zone": "bar",
},
Err: true,
},
"Not a set": {
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
Config: map[string]interface{}{
"ports": "foo",
},
Err: true,
},
"Maps": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
},
},
Config: map[string]interface{}{
"user_data": "foo",
},
Err: true,
},
"Good map: data surrounded by extra slice": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
},
},
Config: map[string]interface{}{
"user_data": []interface{}{
map[string]interface{}{
"foo": "bar",
},
},
},
},
"Good map": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
},
},
Config: map[string]interface{}{
"user_data": map[string]interface{}{
"foo": "bar",
},
},
},
"Map with type specified as value type": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
Elem: TypeBool,
},
},
Config: map[string]interface{}{
"user_data": map[string]interface{}{
"foo": "not_a_bool",
},
},
Err: true,
},
"Map with type specified as nested Schema": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
Elem: &Schema{Type: TypeBool},
},
},
Config: map[string]interface{}{
"user_data": map[string]interface{}{
"foo": "not_a_bool",
},
},
Err: true,
},
"Bad map: just a slice": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
Optional: true,
},
},
Config: map[string]interface{}{
"user_data": []interface{}{
"foo",
},
},
Err: true,
},
"Good set: config has slice with single interpolated value": {
Schema: map[string]*Schema{
"security_groups": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &Schema{Type: TypeString},
Set: func(v interface{}) int {
return len(v.(string))
},
},
},
Config: map[string]interface{}{
"security_groups": []interface{}{"${var.foo}"},
},
Err: false,
},
"Bad set: config has single interpolated value": {
Schema: map[string]*Schema{
"security_groups": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &Schema{Type: TypeString},
},
},
Config: map[string]interface{}{
"security_groups": "${var.foo}",
},
Err: true,
},
"Bad, subresource should not allow unknown elements": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{
"port": 80,
"other": "yes",
},
},
},
Err: true,
},
"Bad, subresource should not allow invalid types": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Optional: true,
Elem: &Resource{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Required: true,
},
},
},
},
},
Config: map[string]interface{}{
"ingress": []interface{}{
map[string]interface{}{
"port": "bad",
},
},
},
Err: true,
},
"Bad, should not allow lists to be assigned to string attributes": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
},
},
Config: map[string]interface{}{
"availability_zone": []interface{}{"foo", "bar", "baz"},
},
Err: true,
},
"Bad, should not allow maps to be assigned to string attributes": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
},
},
Config: map[string]interface{}{
"availability_zone": map[string]interface{}{"foo": "bar", "baz": "thing"},
},
Err: true,
},
"Deprecated attribute usage generates warning, but not error": {
Schema: map[string]*Schema{
"old_news": &Schema{
Type: TypeString,
Optional: true,
Deprecated: "please use 'new_news' instead",
},
},
Config: map[string]interface{}{
"old_news": "extra extra!",
},
Err: false,
Warnings: []string{
"\"old_news\": [DEPRECATED] please use 'new_news' instead",
},
},
"Deprecated generates no warnings if attr not used": {
Schema: map[string]*Schema{
"old_news": &Schema{
Type: TypeString,
Optional: true,
Deprecated: "please use 'new_news' instead",
},
},
Err: false,
Warnings: nil,
},
"Removed attribute usage generates error": {
Schema: map[string]*Schema{
"long_gone": &Schema{
Type: TypeString,
Optional: true,
Removed: "no longer supported by Cloud API",
},
},
Config: map[string]interface{}{
"long_gone": "still here!",
},
Err: true,
Errors: []error{
fmt.Errorf("\"long_gone\": [REMOVED] no longer supported by Cloud API"),
},
},
"Removed generates no errors if attr not used": {
Schema: map[string]*Schema{
"long_gone": &Schema{
Type: TypeString,
Optional: true,
Removed: "no longer supported by Cloud API",
},
},
Err: false,
},
"Conflicting attributes generate error": {
Schema: map[string]*Schema{
"whitelist": &Schema{
Type: TypeString,
Optional: true,
},
"blacklist": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"whitelist"},
},
},
Config: map[string]interface{}{
"whitelist": "white-val",
"blacklist": "black-val",
},
Err: true,
Errors: []error{
fmt.Errorf("\"blacklist\": conflicts with whitelist"),
},
},
"Required attribute & undefined conflicting optional are good": {
Schema: map[string]*Schema{
"required_att": &Schema{
Type: TypeString,
Required: true,
},
"optional_att": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"required_att"},
},
},
Config: map[string]interface{}{
"required_att": "required-val",
},
Err: false,
},
"Required conflicting attribute & defined optional generate error": {
Schema: map[string]*Schema{
"required_att": &Schema{
Type: TypeString,
Required: true,
},
"optional_att": &Schema{
Type: TypeString,
Optional: true,
ConflictsWith: []string{"required_att"},
},
},
Config: map[string]interface{}{
"required_att": "required-val",
"optional_att": "optional-val",
},
Err: true,
Errors: []error{
fmt.Errorf(`"optional_att": conflicts with required_att`),
},
},
"Computed + Optional fields conflicting with each other": {
Schema: map[string]*Schema{
"foo_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"bar_att"},
},
"bar_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"foo_att"},
},
},
Config: map[string]interface{}{
"foo_att": "foo-val",
"bar_att": "bar-val",
},
Err: true,
Errors: []error{
fmt.Errorf(`"foo_att": conflicts with bar_att`),
fmt.Errorf(`"bar_att": conflicts with foo_att`),
},
},
"Computed + Optional fields NOT conflicting with each other": {
Schema: map[string]*Schema{
"foo_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"bar_att"},
},
"bar_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"foo_att"},
},
},
Config: map[string]interface{}{
"foo_att": "foo-val",
},
Err: false,
},
"Computed + Optional fields that conflict with none set": {
Schema: map[string]*Schema{
"foo_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"bar_att"},
},
"bar_att": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"foo_att"},
},
},
Config: map[string]interface{}{},
Err: false,
},
"Good with ValidateFunc": {
Schema: map[string]*Schema{
"validate_me": &Schema{
Type: TypeString,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
return
},
},
},
Config: map[string]interface{}{
"validate_me": "valid",
},
Err: false,
},
"Bad with ValidateFunc": {
Schema: map[string]*Schema{
"validate_me": &Schema{
Type: TypeString,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
es = append(es, fmt.Errorf("something is not right here"))
return
},
},
},
Config: map[string]interface{}{
"validate_me": "invalid",
},
Err: true,
Errors: []error{
fmt.Errorf(`something is not right here`),
},
},
"ValidateFunc not called when type does not match": {
Schema: map[string]*Schema{
"number": &Schema{
Type: TypeInt,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
t.Fatalf("Should not have gotten validate call")
return
},
},
},
Config: map[string]interface{}{
"number": "NaN",
},
Err: true,
},
"ValidateFunc gets decoded type": {
Schema: map[string]*Schema{
"maybe": &Schema{
Type: TypeBool,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
if _, ok := v.(bool); !ok {
t.Fatalf("Expected bool, got: %#v", v)
}
return
},
},
},
Config: map[string]interface{}{
"maybe": "true",
},
},
"ValidateFunc is not called with a computed value": {
Schema: map[string]*Schema{
"validate_me": &Schema{
Type: TypeString,
Required: true,
2015-06-24 18:29:24 +02:00
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
es = append(es, fmt.Errorf("something is not right here"))
return
},
},
},
Config: map[string]interface{}{
"validate_me": "${var.foo}",
},
Vars: map[string]string{
"var.foo": config.UnknownVariableValue,
},
Err: false,
},
"special timeouts field": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Config: map[string]interface{}{
TimeoutsConfigKey: "bar",
},
Err: false,
},
"invalid bool field": {
Schema: map[string]*Schema{
"bool_field": {
Type: TypeBool,
Optional: true,
},
},
Config: map[string]interface{}{
"bool_field": "abcdef",
},
Err: true,
},
"invalid integer field": {
Schema: map[string]*Schema{
"integer_field": {
Type: TypeInt,
Optional: true,
},
},
Config: map[string]interface{}{
"integer_field": "abcdef",
},
Err: true,
},
"invalid float field": {
Schema: map[string]*Schema{
"float_field": {
Type: TypeFloat,
Optional: true,
},
},
Config: map[string]interface{}{
"float_field": "abcdef",
},
Err: true,
},
// Invalid map values
"invalid bool map value": {
Schema: map[string]*Schema{
"boolMap": &Schema{
Type: TypeMap,
Elem: TypeBool,
Optional: true,
},
},
Config: map[string]interface{}{
"boolMap": map[string]interface{}{
"boolField": "notbool",
},
},
Err: true,
},
"invalid int map value": {
Schema: map[string]*Schema{
"intMap": &Schema{
Type: TypeMap,
Elem: TypeInt,
Optional: true,
},
},
Config: map[string]interface{}{
"intMap": map[string]interface{}{
"intField": "notInt",
},
},
Err: true,
},
"invalid float map value": {
Schema: map[string]*Schema{
"floatMap": &Schema{
Type: TypeMap,
Elem: TypeFloat,
Optional: true,
},
},
Config: map[string]interface{}{
"floatMap": map[string]interface{}{
"floatField": "notFloat",
},
},
Err: true,
},
"map with positive validate function": {
Schema: map[string]*Schema{
"floatInt": &Schema{
Type: TypeMap,
Elem: TypeInt,
Optional: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
return
},
},
},
Config: map[string]interface{}{
"floatInt": map[string]interface{}{
"rightAnswer": "42",
"tooMuch": "43",
},
},
Err: false,
},
"map with negative validate function": {
Schema: map[string]*Schema{
"floatInt": &Schema{
Type: TypeMap,
Elem: TypeInt,
Optional: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
es = append(es, fmt.Errorf("this is not fine"))
return
},
},
},
Config: map[string]interface{}{
"floatInt": map[string]interface{}{
"rightAnswer": "42",
"tooMuch": "43",
},
},
Err: true,
},
// The Validation function should not see interpolation strings from
// non-computed values.
"set with partially computed list and map": {
Schema: map[string]*Schema{
"outer": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"list": &Schema{
Type: TypeList,
Optional: true,
Elem: &Schema{
Type: TypeString,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
if strings.HasPrefix(v.(string), "${") {
es = append(es, fmt.Errorf("should not have interpolations"))
}
return
},
},
},
},
},
},
},
Config: map[string]interface{}{
"outer": []map[string]interface{}{
{
"list": []interface{}{"${var.a}", "${var.b}", "c"},
},
},
},
Vars: map[string]string{
"var.a": "A",
"var.b": config.UnknownVariableValue,
},
Err: false,
},
2014-08-16 07:00:16 +02:00
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
if tc.Vars != nil {
vars := make(map[string]ast.Variable)
for k, v := range tc.Vars {
vars[k] = ast.Variable{Value: v, Type: ast.TypeString}
}
2014-08-16 07:00:16 +02:00
if err := c.Interpolate(vars); err != nil {
t.Fatalf("err: %s", err)
}
2014-08-16 07:00:16 +02:00
}
ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if len(es) > 0 != tc.Err {
if len(es) == 0 {
t.Errorf("%q: no errors", tn)
}
2014-08-16 07:00:16 +02:00
for _, e := range es {
t.Errorf("%q: err: %s", tn, e)
}
2014-08-16 07:00:16 +02:00
t.FailNow()
}
if !reflect.DeepEqual(ws, tc.Warnings) {
t.Fatalf("%q: warnings:\n\nexpected: %#v\ngot:%#v", tn, tc.Warnings, ws)
}
if tc.Errors != nil {
sort.Sort(errorSort(es))
sort.Sort(errorSort(tc.Errors))
if !reflect.DeepEqual(es, tc.Errors) {
t.Fatalf("%q: errors:\n\nexpected: %q\ngot: %q", tn, tc.Errors, es)
}
}
})
2014-08-16 07:00:16 +02:00
}
}
func TestSchemaSet_ValidateMaxItems(t *testing.T) {
cases := map[string]struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
ConfigVariables map[string]string
Diff *terraform.InstanceDiff
Err bool
Errors []error
}{
"#0": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
MaxItems: 1,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo", "bar"},
},
Diff: nil,
Err: true,
Errors: []error{
fmt.Errorf("aliases: attribute supports 1 item maximum, config has 2 declared"),
},
},
"#1": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo", "bar"},
},
Diff: nil,
Err: false,
Errors: nil,
},
"#2": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
MaxItems: 1,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo"},
},
Diff: nil,
Err: false,
Errors: nil,
},
}
for tn, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("%q: err: %s", tn, err)
}
_, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if len(es) > 0 != tc.Err {
if len(es) == 0 {
t.Errorf("%q: no errors", tn)
}
for _, e := range es {
t.Errorf("%q: err: %s", tn, e)
}
t.FailNow()
}
if tc.Errors != nil {
if !reflect.DeepEqual(es, tc.Errors) {
t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es)
}
}
}
}
func TestSchemaSet_ValidateMinItems(t *testing.T) {
cases := map[string]struct {
Schema map[string]*Schema
State *terraform.InstanceState
Config map[string]interface{}
ConfigVariables map[string]string
Diff *terraform.InstanceDiff
Err bool
Errors []error
}{
"#0": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
MinItems: 2,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo", "bar"},
},
Diff: nil,
Err: false,
Errors: nil,
},
"#1": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo", "bar"},
},
Diff: nil,
Err: false,
Errors: nil,
},
"#2": {
Schema: map[string]*Schema{
"aliases": &Schema{
Type: TypeSet,
Optional: true,
MinItems: 2,
Elem: &Schema{Type: TypeString},
},
},
State: nil,
Config: map[string]interface{}{
"aliases": []interface{}{"foo"},
},
Diff: nil,
Err: true,
Errors: []error{
fmt.Errorf("aliases: attribute supports 2 item as a minimum, config has 1 declared"),
},
},
}
for tn, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("%q: err: %s", tn, err)
}
_, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if len(es) > 0 != tc.Err {
if len(es) == 0 {
t.Errorf("%q: no errors", tn)
}
for _, e := range es {
t.Errorf("%q: err: %s", tn, e)
}
t.FailNow()
}
if tc.Errors != nil {
if !reflect.DeepEqual(es, tc.Errors) {
t.Fatalf("%q: expected: %q\ngot: %q", tn, tc.Errors, es)
}
}
}
}
// errorSort implements sort.Interface to sort errors by their error message
type errorSort []error
func (e errorSort) Len() int { return len(e) }
func (e errorSort) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
func (e errorSort) Less(i, j int) bool {
return e[i].Error() < e[j].Error()
}
func TestSchemaMapDeepCopy(t *testing.T) {
schema := map[string]*Schema{
"foo": &Schema{
Type: TypeString,
},
}
source := schemaMap(schema)
dest := source.DeepCopy()
dest["foo"].ForceNew = true
if reflect.DeepEqual(source, dest) {
t.Fatalf("source and dest should not match")
}
}