diff --git a/Makefile b/Makefile index 028fd3feb1..012e4a9ce4 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ default: test libucl: vendor/libucl/$(LIBUCL_NAME) test: libucl - go test $(TEST) + go test $(TEST) -timeout=5s vendor/libucl/libucl.a: vendor/libucl cd vendor/libucl && \ diff --git a/terraform/diff.go b/terraform/diff.go index 725800d2f6..9e54ece7bf 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -1,15 +1,26 @@ package terraform +import ( + "sync" +) + // Diff tracks the differences between resources to apply. type Diff struct { - resources map[string]map[string]*resourceDiff + Resources map[string]map[string]*ResourceAttrDiff + once sync.Once +} + +func (d *Diff) init() { + d.once.Do(func() { + d.Resources = make(map[string]map[string]*ResourceAttrDiff) + }) } -// resourceDiff is the diff of a single attribute of a resource. +// ResourceAttrDiff is the diff of a single attribute of a resource. // // This tracks the old value, the new value, and whether the change of this // value actually requires an entirely new resource. -type resourceDiff struct { +type ResourceAttrDiff struct { Old string New string RequiresNew bool diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 67100de259..e4cccedcaa 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -30,14 +30,7 @@ type ResourceProvider interface { // ResourceDiff is the diff of a resource from some state to another. type ResourceDiff struct { - Attributes map[string]ResourceDiffAttribute -} - -// ResourceDiffAttribute is the diff of a single attribute of a resource. -type ResourceDiffAttribute struct { - Old string - New string - RequiresNew bool + Attributes map[string]*ResourceAttrDiff } // ResourceState holds the state of a resource that is used so that diff --git a/terraform/resource_provider_mock.go b/terraform/resource_provider_mock.go index f648ca9550..b3e62ae567 100644 --- a/terraform/resource_provider_mock.go +++ b/terraform/resource_provider_mock.go @@ -10,11 +10,11 @@ type MockResourceProvider struct { ConfigureConfig map[string]interface{} ConfigureReturnWarnings []string ConfigureReturnError error - ResourceDiffCalled bool - ResourceDiffState ResourceState - ResourceDiffDesired map[string]interface{} - ResourceDiffReturn ResourceDiff - ResourceDiffReturnError error + DiffCalled bool + DiffState ResourceState + DiffDesired map[string]interface{} + DiffReturn ResourceDiff + DiffReturnError error ResourcesCalled bool ResourcesReturn []ResourceType } @@ -28,10 +28,10 @@ func (p *MockResourceProvider) Configure(c map[string]interface{}) ([]string, er func (p *MockResourceProvider) Diff( state ResourceState, desired map[string]interface{}) (ResourceDiff, error) { - p.ResourceDiffCalled = true - p.ResourceDiffState = state - p.ResourceDiffDesired = desired - return p.ResourceDiffReturn, p.ResourceDiffReturnError + p.DiffCalled = true + p.DiffState = state + p.DiffDesired = desired + return p.DiffReturn, p.DiffReturnError } func (p *MockResourceProvider) Resources() []ResourceType { diff --git a/terraform/terraform.go b/terraform/terraform.go index 3634a133f3..d8201687ac 100644 --- a/terraform/terraform.go +++ b/terraform/terraform.go @@ -3,6 +3,7 @@ package terraform import ( "fmt" "strings" + "sync" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/depgraph" @@ -124,14 +125,57 @@ func (t *Terraform) Apply(*State, *Diff) (*State, error) { return nil, nil } -func (t *Terraform) Diff(*State) (*Diff, error) { - return nil, nil +func (t *Terraform) Diff(s *State) (*Diff, error) { + result := new(Diff) + err := t.graph.Walk(t.diffWalkFn(s, result)) + if err != nil { + return nil, err + } + + return result, nil } func (t *Terraform) Refresh(*State) (*State, error) { return nil, nil } +func (t *Terraform) diffWalkFn( + state *State, result *Diff) depgraph.WalkFunc { + var resultLock sync.Mutex + + return func(n *depgraph.Noun) error { + // If it is the root node, ignore + if n.Name == config.ResourceGraphRoot { + return nil + } + + r := n.Meta.(*config.Resource) + p := t.mapping[r] + if p == nil { + panic(fmt.Sprintf("No provider for resource: %s", r.Id())) + } + + var rs ResourceState + diff, err := p.Diff(rs, r.Config) + if err != nil { + return err + } + + // If there were no diff items, return right away + if len(diff.Attributes) == 0 { + return nil + } + + // Acquire a lock and modify the resulting diff + resultLock.Lock() + defer resultLock.Unlock() + result.init() + result.Resources[r.Id()] = diff.Attributes + + return nil + } +} + // matchingPrefixes takes a resource type and a set of resource // providers we know about by prefix and returns a list of prefixes // that might be valid for that resource. diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 1821257d30..a918a585bb 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -10,46 +10,6 @@ import ( // This is the directory where our test fixtures are. const fixtureDir = "./test-fixtures" -func testConfig(t *testing.T, name string) *config.Config { - c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf")) - if err != nil { - t.Fatalf("err: %s", err) - } - - return c -} - -func testProviderFunc(n string, rs []string) ResourceProviderFactory { - resources := make([]ResourceType, len(rs)) - for i, v := range rs { - resources[i] = ResourceType{ - Name: v, - } - } - - return func() (ResourceProvider, error) { - result := &MockResourceProvider{ - Meta: n, - ResourcesReturn: resources, - } - - return result, nil - } -} - -func testProviderName(p ResourceProvider) string { - return p.(*MockResourceProvider).Meta.(string) -} - -func testResourceMapping(tf *Terraform) map[string]ResourceProvider { - result := make(map[string]ResourceProvider) - for resource, provider := range tf.mapping { - result[resource.Id()] = provider - } - - return result -} - func TestNew(t *testing.T) { config := testConfig(t, "new-good") tfConfig := &Config{ @@ -141,3 +101,86 @@ func TestNew_variables(t *testing.T) { t.Fatal("tf should not be nil") } } + +func TestTerraformDiff(t *testing.T) { + tf := testTerraform(t, "diff-good") + + diff, err := tf.Diff(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(diff.Resources) < 2 { + t.Fatalf("bad: %#v", diff.Resources) + } +} + +func testConfig(t *testing.T, name string) *config.Config { + c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + return c +} + +func testProviderFunc(n string, rs []string) ResourceProviderFactory { + resources := make([]ResourceType, len(rs)) + for i, v := range rs { + resources[i] = ResourceType{ + Name: v, + } + } + + return func() (ResourceProvider, error) { + var diff ResourceDiff + diff.Attributes = map[string]*ResourceAttrDiff{ + n: &ResourceAttrDiff{ + Old: "foo", + New: "bar", + }, + } + + result := &MockResourceProvider{ + Meta: n, + DiffReturn: diff, + ResourcesReturn: resources, + } + + return result, nil + } +} + +func testProviderName(p ResourceProvider) string { + return p.(*MockResourceProvider).Meta.(string) +} + +func testResourceMapping(tf *Terraform) map[string]ResourceProvider { + result := make(map[string]ResourceProvider) + for resource, provider := range tf.mapping { + result[resource.Id()] = provider + } + + return result +} + +func testTerraform(t *testing.T, name string) *Terraform { + config := testConfig(t, name) + tfConfig := &Config{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFunc("aws", []string{"aws_instance"}), + "do": testProviderFunc("do", []string{"do_droplet"}), + }, + } + + tf, err := New(tfConfig) + if err != nil { + t.Fatalf("err: %s", err) + } + if tf == nil { + t.Fatal("tf should not be nil") + } + + return tf +} diff --git a/terraform/test-fixtures/diff-good/main.tf b/terraform/test-fixtures/diff-good/main.tf new file mode 100644 index 0000000000..642251b951 --- /dev/null +++ b/terraform/test-fixtures/diff-good/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "foo" { + num = 2 +} + +resource "aws_instance" "bar" { + foo = "${aws_instance.foo.num}" +}