terraform: Context.Plan

This commit is contained in:
Mitchell Hashimoto 2014-07-03 10:44:30 -07:00
parent 2e10ddb878
commit 403876fff3
3 changed files with 366 additions and 167 deletions

View File

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

View File

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

View File

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