terraform: Diff!

This commit is contained in:
Mitchell Hashimoto 2014-06-05 02:32:10 -07:00
parent 0d9fb53a5a
commit e904fca3da
7 changed files with 161 additions and 63 deletions

View File

@ -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 && \

View File

@ -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
}
// resourceDiff is the diff of a single attribute of a resource.
func (d *Diff) init() {
d.once.Do(func() {
d.Resources = make(map[string]map[string]*ResourceAttrDiff)
})
}
// 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

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = 2
}
resource "aws_instance" "bar" {
foo = "${aws_instance.foo.num}"
}