terraform/terraform/resource_test.go

675 lines
16 KiB
Go

package terraform
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/mitchellh/reflectwalk"
)
func TestResourceConfigGet(t *testing.T) {
fooStringSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
}
fooListSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.List(cty.Number), Optional: true},
},
}
cases := []struct {
Config cty.Value
Schema *configschema.Block
Key string
Value interface{}
}{
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
Schema: fooStringSchema,
Key: "foo",
Value: "bar",
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
Schema: fooStringSchema,
Key: "foo",
Value: hcl2shim.UnknownVariableValue,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.0",
Value: 1,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.5",
Value: nil,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(5),
}),
}),
Schema: fooListSchema,
Key: "foo.-1",
Value: nil,
},
// get from map
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key": cty.NumberIntVal(1),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key",
Value: 1,
},
// get from map with dot in key
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name",
Value: 1,
},
// get from map with overlapping key names
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
"key.name.2": cty.NumberIntVal(2),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name.2",
Value: 2,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key.name": cty.NumberIntVal(1),
"key.name.foo": cty.NumberIntVal(2),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
},
},
Key: "mapname.0.key.name",
Value: 1,
},
{
Config: cty.ObjectVal(map[string]cty.Value{
"mapname": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"listkey": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"key": cty.NumberIntVal(3),
}),
}),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"mapname": {Type: cty.List(cty.Map(cty.List(cty.Map(cty.Number)))), Optional: true},
},
},
Key: "mapname.0.listkey.0.key",
Value: 3,
},
}
for i, tc := range cases {
rc := NewResourceConfigShimmed(tc.Config, tc.Schema)
// Test getting a key
t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
v, ok := rc.Get(tc.Key)
if ok && v == nil {
t.Fatal("(nil, true) returned from Get")
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("%d bad: %#v", i, v)
}
})
// Test copying and equality
t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) {
copy := rc.DeepCopy()
if !reflect.DeepEqual(copy, rc) {
t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc)
}
if !copy.Equal(rc) {
t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc)
}
if !rc.Equal(copy) {
t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc)
}
})
}
}
func TestResourceConfigDeepCopy_nil(t *testing.T) {
var nilRc *ResourceConfig
actual := nilRc.DeepCopy()
if actual != nil {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceConfigDeepCopy_nilComputed(t *testing.T) {
rc := &ResourceConfig{}
actual := rc.DeepCopy()
if actual.ComputedKeys != nil {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceConfigEqual_nil(t *testing.T) {
var nilRc *ResourceConfig
notNil := NewResourceConfigShimmed(cty.EmptyObjectVal, &configschema.Block{})
if nilRc.Equal(notNil) {
t.Fatal("should not be equal")
}
if notNil.Equal(nilRc) {
t.Fatal("should not be equal")
}
}
func TestResourceConfigEqual_computedKeyOrder(t *testing.T) {
v := cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
})
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
}
rc := NewResourceConfigShimmed(v, schema)
rc2 := NewResourceConfigShimmed(v, schema)
// Set the computed keys manually to force ordering to differ
rc.ComputedKeys = []string{"foo", "bar"}
rc2.ComputedKeys = []string{"bar", "foo"}
if !rc.Equal(rc2) {
t.Fatal("should be equal")
}
}
func TestUnknownCheckWalker(t *testing.T) {
cases := []struct {
Name string
Input interface{}
Result bool
}{
{
"primitive",
42,
false,
},
{
"primitive computed",
hcl2shim.UnknownVariableValue,
true,
},
{
"list",
[]interface{}{"foo", hcl2shim.UnknownVariableValue},
true,
},
{
"nested list",
[]interface{}{
"foo",
[]interface{}{hcl2shim.UnknownVariableValue},
},
true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
var w unknownCheckWalker
if err := reflectwalk.Walk(tc.Input, &w); err != nil {
t.Fatalf("err: %s", err)
}
if w.Unknown != tc.Result {
t.Fatalf("bad: %v", w.Unknown)
}
})
}
}
func TestNewResourceConfigShimmed(t *testing.T) {
for _, tc := range []struct {
Name string
Val cty.Value
Schema *configschema.Block
Expected *ResourceConfig
}{
{
Name: "empty object",
Val: cty.NullVal(cty.EmptyObject),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "basic",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{
"foo": "bar",
},
Config: map[string]interface{}{
"foo": "bar",
},
},
},
{
Name: "null string",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "unknown string",
Val: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"foo"},
Raw: map[string]interface{}{
"foo": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"foo": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "unknown collections",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.UnknownVal(cty.Map(cty.String)),
"baz": cty.UnknownVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar", "baz"},
Raw: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "null collections",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.Map(cty.String)),
"baz": cty.NullVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
{
Name: "unknown blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.UnknownVal(cty.Map(cty.String)),
"baz": cty.UnknownVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{},
Nesting: configschema.NestingList,
},
"baz": {
Block: configschema.Block{},
Nesting: configschema.NestingSet,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar", "baz"},
Raw: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
Config: map[string]interface{}{
"bar": hcl2shim.UnknownVariableValue,
"baz": hcl2shim.UnknownVariableValue,
},
},
},
{
Name: "unknown in nested blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"baz": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"list": cty.UnknownVal(cty.List(cty.String)),
}),
}),
}),
}),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"baz": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"list": {Type: cty.List(cty.String),
Optional: true,
},
},
},
Nesting: configschema.NestingList,
},
},
},
Nesting: configschema.NestingList,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.baz.0.list"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"baz": []interface{}{map[string]interface{}{
"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
}},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"baz": []interface{}{map[string]interface{}{
"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
}},
},
},
},
{
Name: "unknown in set",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val": cty.UnknownVal(cty.String),
}),
}),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"val": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSet,
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.val"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
},
},
},
{
Name: "unknown in attribute sets",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"val": cty.UnknownVal(cty.String),
}),
}),
"baz": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"obj": cty.UnknownVal(cty.Object(map[string]cty.Type{
"attr": cty.List(cty.String),
})),
}),
cty.ObjectVal(map[string]cty.Value{
"obj": cty.ObjectVal(map[string]cty.Value{
"attr": cty.UnknownVal(cty.List(cty.String)),
}),
}),
}),
}),
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": &configschema.Attribute{
Type: cty.Set(cty.Object(map[string]cty.Type{
"val": cty.String,
})),
},
"baz": &configschema.Attribute{
Type: cty.Set(cty.Object(map[string]cty.Type{
"obj": cty.Object(map[string]cty.Type{
"attr": cty.List(cty.String),
}),
})),
},
},
},
Expected: &ResourceConfig{
ComputedKeys: []string{"bar.0.val", "baz.0.obj.attr", "baz.1.obj"},
Raw: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
"baz": []interface{}{
map[string]interface{}{
"obj": map[string]interface{}{
"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
map[string]interface{}{
"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
},
Config: map[string]interface{}{
"bar": []interface{}{map[string]interface{}{
"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
}},
"baz": []interface{}{
map[string]interface{}{
"obj": map[string]interface{}{
"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
map[string]interface{}{
"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
},
},
},
},
},
{
Name: "null blocks",
Val: cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.Map(cty.String)),
"baz": cty.NullVal(cty.List(cty.String)),
}),
Schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"bar": {
Block: configschema.Block{},
Nesting: configschema.NestingMap,
},
"baz": {
Block: configschema.Block{},
Nesting: configschema.NestingSingle,
},
},
},
Expected: &ResourceConfig{
Raw: map[string]interface{}{},
Config: map[string]interface{}{},
},
},
} {
t.Run(tc.Name, func(*testing.T) {
cfg := NewResourceConfigShimmed(tc.Val, tc.Schema)
if !tc.Expected.Equal(cfg) {
t.Fatalf("expected:\n%#v\ngot:\n%#v", tc.Expected, cfg)
}
})
}
}