terraform/helper/schema/schema_test.go

1266 lines
21 KiB
Go

package schema
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
func TestSchemaMap_Diff(t *testing.T) {
cases := []struct {
Schema map[string]*Schema
State *terraform.ResourceState
Config map[string]interface{}
Diff *terraform.ResourceDiff
Err bool
}{
/*
* String decode
*/
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"availability_zone": "foo",
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo",
RequiresNew: true,
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{},
Diff: &terraform.ResourceDiff{
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.ResourceState{
ID: "foo",
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
// 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",
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"availability_zone": &terraform.ResourceAttrDiff{
Old: "",
New: "foo!",
NewExtra: "foo",
},
},
},
Err: false,
},
/*
* Int decode
*/
{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"port": 27,
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"port": &terraform.ResourceAttrDiff{
Old: "",
New: "27",
RequiresNew: true,
},
},
},
Err: false,
},
/*
* Bool decode
*/
{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: map[string]interface{}{
"port": false,
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"port": &terraform.ResourceAttrDiff{
Old: "",
New: "0",
RequiresNew: true,
},
},
},
Err: false,
},
/*
* 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},
},
Diff: &terraform.ResourceDiff{
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: &terraform.ResourceState{
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,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"ports.#": "2",
"ports.0": "1",
"ports.1": "2",
},
},
Config: map[string]interface{}{
"ports": []interface{}{1, 2, 5},
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "3",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
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},
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
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,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: map[string]interface{}{},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "",
NewComputed: true,
},
},
},
Err: false,
},
/*
* 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},
},
Diff: &terraform.ResourceDiff{
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: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"ports.#": "2",
"ports.0": "2",
"ports.1": "1",
},
},
Config: map[string]interface{}{
"ports": []interface{}{5, 2, 1},
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "3",
},
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Required: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"ports.#": "2",
"ports.0": "2",
"ports.1": "1",
},
},
Config: map[string]interface{}{},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "2",
New: "0",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "1",
NewRemoved: true,
},
"ports.1": &terraform.ResourceAttrDiff{
Old: "2",
NewRemoved: true,
},
},
},
Err: false,
},
{
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeInt},
Set: func(v interface{}) int { return v.(int) },
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"availability_zone": "bar",
"ports.#": "1",
"ports.0": "80",
},
},
Config: map[string]interface{}{},
Diff: nil,
Err: false,
},
/*
* List of structure decode
*/
{
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,
},
},
},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ingress.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ingress.0.from": &terraform.ResourceAttrDiff{
Old: "",
New: "8080",
},
},
},
Err: false,
},
/*
* ComputedWhen
*/
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
ComputedWhen: []string{"port"},
},
"port": &Schema{
Type: TypeInt,
Optional: true,
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"availability_zone": "foo",
"port": "80",
},
},
Config: map[string]interface{}{
"port": 80,
},
Diff: nil,
Err: false,
},
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
ComputedWhen: []string{"port"},
},
"port": &Schema{
Type: TypeInt,
Optional: true,
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"port": "80",
},
},
Config: map[string]interface{}{
"port": 80,
},
Diff: &terraform.ResourceDiff{
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.ResourceState{
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,
},
*/
/*
* Maps
*/
{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"config_vars.#": "1",
"config_vars.0.foo": "bar",
},
},
Config: map[string]interface{}{
"config_vars": []interface{}{
map[string]interface{}{
"bar": "baz",
},
},
},
Diff: &terraform.ResourceDiff{
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,
},
{
Schema: map[string]*Schema{
"config_vars": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeMap},
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"config_vars.#": "1",
"config_vars.0.foo": "bar",
"config_vars.0.bar": "baz",
},
},
Config: map[string]interface{}{},
Diff: &terraform.ResourceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"config_vars.#": &terraform.ResourceAttrDiff{
Old: "1",
New: "0",
},
"config_vars.0.foo": &terraform.ResourceAttrDiff{
Old: "bar",
NewRemoved: true,
},
"config_vars.0.bar": &terraform.ResourceAttrDiff{
Old: "baz",
NewRemoved: true,
},
},
},
Err: false,
},
/*
* ForceNews
*/
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
ForceNew: true,
},
"address": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"availability_zone": "bar",
"address": "foo",
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
Diff: &terraform.ResourceDiff{
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,
},
// 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(v interface{}) int { return v.(int) },
},
},
State: &terraform.ResourceState{
Attributes: map[string]string{
"availability_zone": "bar",
"ports.#": "1",
"ports.0": "80",
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
Diff: &terraform.ResourceDiff{
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,
},
}
for i, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
d, err := schemaMap(tc.Schema).Diff(
tc.State, terraform.NewResourceConfig(c))
if (err != nil) != tc.Err {
t.Fatalf("#%d err: %s", i, err)
}
if !reflect.DeepEqual(tc.Diff, d) {
t.Fatalf("#%d: bad:\n\n%#v", i, d)
}
}
}
func TestSchemaMap_InternalValidate(t *testing.T) {
cases := []struct {
In map[string]*Schema
Err bool
}{
{
nil,
false,
},
// No optional and no 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,
},
// List element not set
{
map[string]*Schema{
"foo": &Schema{
Type: TypeList,
},
},
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,
},
},
true,
},
// Required but computed
{
map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Required: true,
ComputedWhen: []string{"foo"},
},
},
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,
},
}
for i, tc := range cases {
err := schemaMap(tc.In).InternalValidate()
if (err != nil) != tc.Err {
t.Fatalf("%d: bad: %s\n\n%#v", i, err, tc.In)
}
}
}
func TestSchemaMap_Validate(t *testing.T) {
cases := []struct {
Schema map[string]*Schema
Config map[string]interface{}
Warn bool
Err bool
}{
// Good
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Config: map[string]interface{}{
"availability_zone": "foo",
},
},
// Required field not set
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Required: true,
},
},
Config: map[string]interface{}{},
Err: true,
},
// Invalid type
{
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
Required: true,
},
},
Config: map[string]interface{}{
"port": "I am invalid",
},
Err: true,
},
// Optional sub-resource
{
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,
},
// Not a list
{
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
{
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
{
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[string]interface{}{
"from": 80,
},
},
},
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,
},
// Computed field set
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Computed: true,
},
},
Config: map[string]interface{}{
"availability_zone": "bar",
},
Err: true,
},
}
for i, tc := range cases {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if (len(es) > 0) != tc.Err {
if len(es) == 0 {
t.Errorf("%d: no errors", i)
}
for _, e := range es {
t.Errorf("%d: err: %s", i, e)
}
t.FailNow()
}
if (len(ws) > 0) != tc.Warn {
t.Fatalf("%d: ws: %#v", i, ws)
}
}
}