From 403876fff36be5c843df4530926c6c877e605032 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2014 10:44:30 -0700 Subject: [PATCH] terraform: Context.Plan --- terraform/context.go | 85 +++++++++++ terraform/context_test.go | 281 ++++++++++++++++++++++++++++++++++++ terraform/terraform_test.go | 167 --------------------- 3 files changed, 366 insertions(+), 167 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index b8afa7eaa..be795e8fc 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -52,6 +52,29 @@ func NewContext(opts *ContextOpts) *Context { } } +// Plan generates an execution plan for the given context. +// +// The execution plan encapsulates the context and can be stored +// in order to reinstantiate a context later for Apply. +func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { + g, err := Graph(&GraphOpts{ + Config: c.config, + Providers: c.providers, + State: c.state, + }) + if err != nil { + return nil, err + } + + p := &Plan{ + Config: c.config, + Vars: c.variables, + State: c.state, + } + err = g.Walk(c.planWalkFn(p, opts)) + return p, err +} + // Refresh goes through all the resources in the state and refreshes them // to their latest state. This will update the state that this context // works with, along with returning it. @@ -96,6 +119,68 @@ func (c *Context) Validate() ([]string, []error) { return nil, errs } +func (c *Context) planWalkFn(result *Plan, opts *PlanOpts) depgraph.WalkFunc { + var l sync.Mutex + + // If we were given nil options, instantiate it + if opts == nil { + opts = new(PlanOpts) + } + + // Initialize the result + result.init() + + cb := func(r *Resource) (map[string]string, error) { + var diff *ResourceDiff + + for _, h := range c.hooks { + handleHook(h.PreDiff(r.Id, r.State)) + } + + if opts.Destroy { + if r.State.ID != "" { + log.Printf("[DEBUG] %s: Making for destroy", r.Id) + diff = &ResourceDiff{Destroy: true} + } else { + log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id) + } + } else if r.Config == nil { + log.Printf("[DEBUG] %s: Orphan, marking for destroy", r.Id) + + // This is an orphan (no config), so we mark it to be destroyed + diff = &ResourceDiff{Destroy: true} + } else { + log.Printf("[DEBUG] %s: Executing diff", r.Id) + + // Get a diff from the newest state + var err error + diff, err = r.Provider.Diff(r.State, r.Config) + if err != nil { + return nil, err + } + } + + l.Lock() + if !diff.Empty() { + result.Diff.Resources[r.Id] = diff + } + l.Unlock() + + for _, h := range c.hooks { + handleHook(h.PostDiff(r.Id, diff)) + } + + // Determine the new state and update variables + if !diff.Empty() { + r.State = r.State.MergeDiff(diff) + } + + return r.Vars(), nil + } + + return c.genericWalkFn(c.variables, cb) +} + func (c *Context) refreshWalkFn(result *State) depgraph.WalkFunc { var l sync.Mutex diff --git a/terraform/context_test.go b/terraform/context_test.go index 44d34079f..2902485a2 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -3,6 +3,7 @@ package terraform import ( "fmt" "reflect" + "strings" "testing" ) @@ -51,6 +52,213 @@ func TestContextValidate_requiredVar(t *testing.T) { } } +func TestContextPlan(t *testing.T) { + c := testConfig(t, "plan-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.Resources) < 2 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContextPlan_nil(t *testing.T) { + c := testConfig(t, "plan-nil") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(plan.Diff.Resources) != 0 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } +} + +func TestContextPlan_computed(t *testing.T) { + c := testConfig(t, "plan-computed") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.Resources) < 2 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanComputedStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContextPlan_destroy(t *testing.T) { + c := testConfig(t, "plan-destroy") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.one": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + "aws_instance.two": &ResourceState{ + ID: "baz", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(&PlanOpts{Destroy: true}) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.Resources) != 2 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanDestroyStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContextPlan_hook(t *testing.T) { + c := testConfig(t, "plan-good") + h := new(MockHook) + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + _, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PreDiffCalled { + t.Fatal("should be called") + } + if !h.PostDiffCalled { + t.Fatal("should be called") + } +} + +func TestContextPlan_orphan(t *testing.T) { + c := testConfig(t, "plan-orphan") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.baz": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanOrphanStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + +func TestContextPlan_state(t *testing.T) { + c := testConfig(t, "plan-good") + p := testProvider("aws") + p.DiffFn = testDiffFn + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + ID: "bar", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(plan.Diff.Resources) < 2 { + t.Fatalf("bad: %#v", plan.Diff.Resources) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanStateStr) + if actual != expected { + t.Fatalf("bad:\n%s", actual) + } +} + func TestContextRefresh(t *testing.T) { p := testProvider("aws") c := testConfig(t, "refresh-basic") @@ -158,6 +366,79 @@ func testContext(t *testing.T, opts *ContextOpts) *Context { return NewContext(opts) } +func testDiffFn( + s *ResourceState, + c *ResourceConfig) (*ResourceDiff, error) { + var diff ResourceDiff + diff.Attributes = make(map[string]*ResourceAttrDiff) + diff.Attributes["type"] = &ResourceAttrDiff{ + Old: "", + New: s.Type, + } + + for k, v := range c.Raw { + if _, ok := v.(string); !ok { + 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] + } + + attrDiff := &ResourceAttrDiff{ + Old: "", + New: v.(string), + } + + diff.Attributes[k] = attrDiff + } + + for _, k := range c.ComputedKeys { + diff.Attributes[k] = &ResourceAttrDiff{ + Old: "", + NewComputed: true, + } + } + + return &diff, nil +} + func testProvider(prefix string) *MockResourceProvider { p := new(MockResourceProvider) p.RefreshFn = func(s *ResourceState) (*ResourceState, error) { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 1f9b1d83d..8105fabf7 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -442,173 +442,6 @@ func TestTerraformApply_vars(t *testing.T) { } } -func TestTerraformPlan(t *testing.T) { - c := testConfig(t, "plan-good") - tf := testTerraform2(t, nil) - - plan, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestTerraformPlan_nil(t *testing.T) { - c := testConfig(t, "plan-nil") - tf := testTerraform2(t, nil) - - plan, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - if len(plan.Diff.Resources) != 0 { - t.Fatalf("bad: %#v", plan.Diff.Resources) - } -} - -func TestTerraformPlan_computed(t *testing.T) { - c := testConfig(t, "plan-computed") - tf := testTerraform2(t, nil) - - plan, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanComputedStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestTerraformPlan_destroy(t *testing.T) { - c := testConfig(t, "plan-destroy") - tf := testTerraform2(t, nil) - - s := &State{ - Resources: map[string]*ResourceState{ - "aws_instance.one": &ResourceState{ - ID: "bar", - Type: "aws_instance", - }, - "aws_instance.two": &ResourceState{ - ID: "baz", - Type: "aws_instance", - }, - }, - } - - plan, err := tf.Plan(&PlanOpts{ - Destroy: true, - Config: c, - State: s, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.Resources) != 2 { - t.Fatalf("bad: %#v", plan.Diff.Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanDestroyStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestTerraformPlan_hook(t *testing.T) { - c := testConfig(t, "plan-good") - h := new(MockHook) - tf := testTerraform2(t, &Config{ - Hooks: []Hook{h}, - }) - - if _, err := tf.Plan(&PlanOpts{Config: c}); err != nil { - t.Fatalf("err: %s", err) - } - if !h.PreDiffCalled { - t.Fatal("should be called") - } - if !h.PostDiffCalled { - t.Fatal("should be called") - } -} - -func TestTerraformPlan_orphan(t *testing.T) { - c := testConfig(t, "plan-orphan") - tf := testTerraform2(t, nil) - - s := &State{ - Resources: map[string]*ResourceState{ - "aws_instance.baz": &ResourceState{ - ID: "bar", - Type: "aws_instance", - }, - }, - } - - plan, err := tf.Plan(&PlanOpts{ - Config: c, - State: s, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanOrphanStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - -func TestTerraformPlan_state(t *testing.T) { - c := testConfig(t, "plan-good") - tf := testTerraform2(t, nil) - - s := &State{ - Resources: map[string]*ResourceState{ - "aws_instance.foo": &ResourceState{ - ID: "bar", - }, - }, - } - - plan, err := tf.Plan(&PlanOpts{ - Config: c, - State: s, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(plan.Diff.Resources) < 2 { - t.Fatalf("bad: %#v", plan.Diff.Resources) - } - - actual := strings.TrimSpace(plan.String()) - expected := strings.TrimSpace(testTerraformPlanStateStr) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - func TestTerraformRefresh(t *testing.T) { rpAWS := new(MockResourceProvider) rpAWS.ResourcesReturn = []ResourceType{