439 lines
7.7 KiB
Go
439 lines
7.7 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/terraform/flatmap"
|
|
)
|
|
|
|
func TestNewContextRequiredVersion(t *testing.T) {
|
|
cases := []struct {
|
|
Name string
|
|
Module string
|
|
Version string
|
|
Value string
|
|
Err bool
|
|
}{
|
|
{
|
|
"no requirement",
|
|
"",
|
|
"0.1.0",
|
|
"",
|
|
false,
|
|
},
|
|
|
|
{
|
|
"doesn't match",
|
|
"",
|
|
"0.1.0",
|
|
"> 0.6.0",
|
|
true,
|
|
},
|
|
|
|
{
|
|
"matches",
|
|
"",
|
|
"0.7.0",
|
|
"> 0.6.0",
|
|
false,
|
|
},
|
|
|
|
{
|
|
"module matches",
|
|
"context-required-version-module",
|
|
"0.5.0",
|
|
"",
|
|
false,
|
|
},
|
|
|
|
{
|
|
"module doesn't match",
|
|
"context-required-version-module",
|
|
"0.4.0",
|
|
"",
|
|
true,
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
|
// Reset the version for the tests
|
|
old := SemVersion
|
|
SemVersion = version.Must(version.NewVersion(tc.Version))
|
|
defer func() { SemVersion = old }()
|
|
|
|
name := "context-required-version"
|
|
if tc.Module != "" {
|
|
name = tc.Module
|
|
}
|
|
mod := testModule(t, name)
|
|
if tc.Value != "" {
|
|
mod.Config().Terraform.RequiredVersion = tc.Value
|
|
}
|
|
_, err := NewContext(&ContextOpts{
|
|
Module: mod,
|
|
})
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewContextState(t *testing.T) {
|
|
cases := map[string]struct {
|
|
Input *ContextOpts
|
|
Err bool
|
|
}{
|
|
"empty TFVersion": {
|
|
&ContextOpts{
|
|
State: &State{},
|
|
},
|
|
false,
|
|
},
|
|
|
|
"past TFVersion": {
|
|
&ContextOpts{
|
|
State: &State{TFVersion: "0.1.2"},
|
|
},
|
|
false,
|
|
},
|
|
|
|
"equal TFVersion": {
|
|
&ContextOpts{
|
|
State: &State{TFVersion: Version},
|
|
},
|
|
false,
|
|
},
|
|
|
|
"future TFVersion": {
|
|
&ContextOpts{
|
|
State: &State{TFVersion: "99.99.99"},
|
|
},
|
|
true,
|
|
},
|
|
|
|
"future TFVersion, allowed": {
|
|
&ContextOpts{
|
|
State: &State{TFVersion: "99.99.99"},
|
|
StateFutureAllowed: true,
|
|
},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for k, tc := range cases {
|
|
ctx, err := NewContext(tc.Input)
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("%s: err: %s", k, err)
|
|
}
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// Version should always be set to our current
|
|
if ctx.state.TFVersion != Version {
|
|
t.Fatalf("%s: state not set to current version", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testContext2(t *testing.T, opts *ContextOpts) *Context {
|
|
// Enable the shadow graph
|
|
opts.Shadow = true
|
|
|
|
ctx, err := NewContext(opts)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
func testDataApplyFn(
|
|
info *InstanceInfo,
|
|
d *InstanceDiff) (*InstanceState, error) {
|
|
return testApplyFn(info, new(InstanceState), d)
|
|
}
|
|
|
|
func testDataDiffFn(
|
|
info *InstanceInfo,
|
|
c *ResourceConfig) (*InstanceDiff, error) {
|
|
return testDiffFn(info, new(InstanceState), c)
|
|
}
|
|
|
|
func testApplyFn(
|
|
info *InstanceInfo,
|
|
s *InstanceState,
|
|
d *InstanceDiff) (*InstanceState, error) {
|
|
if d.Destroy {
|
|
return nil, nil
|
|
}
|
|
|
|
id := "foo"
|
|
if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed {
|
|
id = idAttr.New
|
|
}
|
|
|
|
result := &InstanceState{
|
|
ID: id,
|
|
Attributes: make(map[string]string),
|
|
}
|
|
|
|
// Copy all the prior attributes
|
|
for k, v := range s.Attributes {
|
|
result.Attributes[k] = v
|
|
}
|
|
|
|
if d != nil {
|
|
result = result.MergeDiff(d)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func testDiffFn(
|
|
info *InstanceInfo,
|
|
s *InstanceState,
|
|
c *ResourceConfig) (*InstanceDiff, error) {
|
|
diff := new(InstanceDiff)
|
|
diff.Attributes = make(map[string]*ResourceAttrDiff)
|
|
|
|
if s != nil {
|
|
diff.DestroyTainted = s.Tainted
|
|
}
|
|
|
|
for k, v := range c.Raw {
|
|
// Ignore __-prefixed keys since they're used for magic
|
|
if k[0] == '_' && k[1] == '_' {
|
|
continue
|
|
}
|
|
|
|
if k == "nil" {
|
|
return nil, nil
|
|
}
|
|
|
|
// This key is used for other purposes
|
|
if k == "compute_value" {
|
|
continue
|
|
}
|
|
|
|
if k == "compute" {
|
|
attrDiff := &ResourceAttrDiff{
|
|
Old: "",
|
|
New: "",
|
|
NewComputed: true,
|
|
}
|
|
|
|
if cv, ok := c.Config["compute_value"]; ok {
|
|
if cv.(string) == "1" {
|
|
attrDiff.NewComputed = false
|
|
attrDiff.New = fmt.Sprintf("computed_%s", v.(string))
|
|
}
|
|
}
|
|
|
|
diff.Attributes[v.(string)] = attrDiff
|
|
continue
|
|
}
|
|
|
|
// If this key is not computed, then look it up in the
|
|
// cleaned config.
|
|
found := false
|
|
for _, ck := range c.ComputedKeys {
|
|
if ck == k {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
v = c.Config[k]
|
|
}
|
|
|
|
for k, attrDiff := range testFlatAttrDiffs(k, v) {
|
|
if k == "require_new" {
|
|
attrDiff.RequiresNew = true
|
|
}
|
|
if _, ok := c.Raw["__"+k+"_requires_new"]; ok {
|
|
attrDiff.RequiresNew = true
|
|
}
|
|
|
|
if attr, ok := s.Attributes[k]; ok {
|
|
attrDiff.Old = attr
|
|
}
|
|
|
|
diff.Attributes[k] = attrDiff
|
|
}
|
|
}
|
|
|
|
for _, k := range c.ComputedKeys {
|
|
diff.Attributes[k] = &ResourceAttrDiff{
|
|
Old: "",
|
|
NewComputed: true,
|
|
}
|
|
}
|
|
|
|
// If we recreate this resource because it's tainted, we keep all attrs
|
|
if !diff.RequiresNew() {
|
|
for k, v := range diff.Attributes {
|
|
if v.NewComputed {
|
|
continue
|
|
}
|
|
|
|
old, ok := s.Attributes[k]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if old == v.New {
|
|
delete(diff.Attributes, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !diff.Empty() {
|
|
diff.Attributes["type"] = &ResourceAttrDiff{
|
|
Old: "",
|
|
New: info.Type,
|
|
}
|
|
}
|
|
|
|
return diff, nil
|
|
}
|
|
|
|
// generate ResourceAttrDiffs for nested data structures in tests
|
|
func testFlatAttrDiffs(k string, i interface{}) map[string]*ResourceAttrDiff {
|
|
diffs := make(map[string]*ResourceAttrDiff)
|
|
// check for strings and empty containers first
|
|
switch t := i.(type) {
|
|
case string:
|
|
diffs[k] = &ResourceAttrDiff{New: t}
|
|
return diffs
|
|
case map[string]interface{}:
|
|
if len(t) == 0 {
|
|
diffs[k] = &ResourceAttrDiff{New: ""}
|
|
return diffs
|
|
}
|
|
case []interface{}:
|
|
if len(t) == 0 {
|
|
diffs[k] = &ResourceAttrDiff{New: ""}
|
|
return diffs
|
|
}
|
|
}
|
|
|
|
flat := flatmap.Flatten(map[string]interface{}{k: i})
|
|
|
|
for k, v := range flat {
|
|
attrDiff := &ResourceAttrDiff{
|
|
Old: "",
|
|
New: v,
|
|
}
|
|
diffs[k] = attrDiff
|
|
}
|
|
|
|
return diffs
|
|
}
|
|
|
|
func testProvider(prefix string) *MockResourceProvider {
|
|
p := new(MockResourceProvider)
|
|
p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) {
|
|
return s, nil
|
|
}
|
|
p.ResourcesReturn = []ResourceType{
|
|
ResourceType{
|
|
Name: fmt.Sprintf("%s_instance", prefix),
|
|
},
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
func testProvisioner() *MockResourceProvisioner {
|
|
p := new(MockResourceProvisioner)
|
|
return p
|
|
}
|
|
|
|
func checkStateString(t *testing.T, state *State, expected string) {
|
|
actual := strings.TrimSpace(state.String())
|
|
expected = strings.TrimSpace(expected)
|
|
|
|
if actual != expected {
|
|
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
|
|
}
|
|
}
|
|
|
|
func resourceState(resourceType, resourceID string) *ResourceState {
|
|
return &ResourceState{
|
|
Type: resourceType,
|
|
Primary: &InstanceState{
|
|
ID: resourceID,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Test helper that gives a function 3 seconds to finish, assumes deadlock and
|
|
// fails test if it does not.
|
|
func testCheckDeadlock(t *testing.T, f func()) {
|
|
timeout := make(chan bool, 1)
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
time.Sleep(3 * time.Second)
|
|
timeout <- true
|
|
}()
|
|
go func(f func(), done chan bool) {
|
|
defer func() { done <- true }()
|
|
f()
|
|
}(f, done)
|
|
select {
|
|
case <-timeout:
|
|
t.Fatalf("timed out! probably deadlock")
|
|
case <-done:
|
|
// ok
|
|
}
|
|
}
|
|
|
|
const testContextGraph = `
|
|
root: root
|
|
aws_instance.bar
|
|
aws_instance.bar -> provider.aws
|
|
aws_instance.foo
|
|
aws_instance.foo -> provider.aws
|
|
provider.aws
|
|
root
|
|
root -> aws_instance.bar
|
|
root -> aws_instance.foo
|
|
`
|
|
|
|
const testContextRefreshModuleStr = `
|
|
aws_instance.web: (tainted)
|
|
ID = bar
|
|
|
|
module.child:
|
|
aws_instance.web:
|
|
ID = new
|
|
`
|
|
|
|
const testContextRefreshOutputStr = `
|
|
aws_instance.web:
|
|
ID = foo
|
|
foo = bar
|
|
|
|
Outputs:
|
|
|
|
foo = bar
|
|
`
|
|
|
|
const testContextRefreshOutputPartialStr = `
|
|
<no state>
|
|
`
|
|
|
|
const testContextRefreshTaintedStr = `
|
|
aws_instance.web: (tainted)
|
|
ID = foo
|
|
`
|