From 603ee36d92310cc8b698f30ee245a9028e506a8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2014 11:04:04 -0700 Subject: [PATCH] terraform: Context.Apply --- terraform/context.go | 122 +++++++++ terraform/context_test.go | 480 ++++++++++++++++++++++++++++++++++++ terraform/terraform_test.go | 429 -------------------------------- 3 files changed, 602 insertions(+), 429 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index be795e8fc..1ad54db59 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -52,10 +52,47 @@ func NewContext(opts *ContextOpts) *Context { } } +// Apply applies the changes represented by this context and returns +// the resulting state. +// +// In addition to returning the resulting state, this context is updated +// with the latest state. +func (c *Context) Apply() (*State, error) { + g, err := Graph(&GraphOpts{ + Config: c.config, + Diff: c.diff, + Providers: c.providers, + State: c.state, + }) + if err != nil { + return nil, err + } + + // Create our result. Make sure we preserve the prior states + s := new(State) + s.init() + if c.state != nil { + for k, v := range c.state.Resources { + s.Resources[k] = v + } + } + + // Walk + err = g.Walk(c.applyWalkFn(s)) + + // Update our state, even if we have an error, for partial updates + c.state = s + + return s, err +} + // 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. +// +// Plan also updates the diff of this context to be the diff generated +// by the plan, so Apply can be called after. func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { g, err := Graph(&GraphOpts{ Config: c.config, @@ -72,6 +109,10 @@ func (c *Context) Plan(opts *PlanOpts) (*Plan, error) { State: c.state, } err = g.Walk(c.planWalkFn(p, opts)) + + // Update the diff so that our context is up-to-date + c.diff = p.Diff + return p, err } @@ -119,6 +160,87 @@ func (c *Context) Validate() ([]string, []error) { return nil, errs } +func (c *Context) applyWalkFn(result *State) depgraph.WalkFunc { + var l sync.Mutex + + // Initialize the result + result.init() + + cb := func(r *Resource) (map[string]string, error) { + diff := r.Diff + if diff.Empty() { + return r.Vars(), nil + } + + if !diff.Destroy { + var err error + diff, err = r.Provider.Diff(r.State, r.Config) + if err != nil { + return nil, err + } + } + + // TODO(mitchellh): we need to verify the diff doesn't change + // anything and that the diff has no computed values (pre-computed) + + for _, h := range c.hooks { + handleHook(h.PreApply(r.Id, r.State, diff)) + } + + // With the completed diff, apply! + log.Printf("[DEBUG] %s: Executing Apply", r.Id) + rs, err := r.Provider.Apply(r.State, diff) + if err != nil { + return nil, err + } + + // Make sure the result is instantiated + if rs == nil { + rs = new(ResourceState) + } + + // Force the resource state type to be our type + rs.Type = r.State.Type + + var errs []error + for ak, av := range rs.Attributes { + // If the value is the unknown variable value, then it is an error. + // In this case we record the error and remove it from the state + if av == config.UnknownVariableValue { + errs = append(errs, fmt.Errorf( + "Attribute with unknown value: %s", ak)) + delete(rs.Attributes, ak) + } + } + + // Update the resulting diff + l.Lock() + if rs.ID == "" { + delete(result.Resources, r.Id) + } else { + result.Resources[r.Id] = rs + } + l.Unlock() + + // Update the state for the resource itself + r.State = rs + + for _, h := range c.hooks { + handleHook(h.PostApply(r.Id, r.State)) + } + + // Determine the new state and update variables + err = nil + if len(errs) > 0 { + err = &multierror.Error{Errors: errs} + } + + return r.Vars(), err + } + + return c.genericWalkFn(c.variables, cb) +} + func (c *Context) planWalkFn(result *Plan, opts *PlanOpts) depgraph.WalkFunc { var l sync.Mutex diff --git a/terraform/context_test.go b/terraform/context_test.go index 2902485a2..990b0ad67 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -52,6 +52,455 @@ func TestContextValidate_requiredVar(t *testing.T) { } } +func TestContextApply(t *testing.T) { + c := testConfig(t, "apply-good") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(state.Resources) < 2 { + t.Fatalf("bad: %#v", state.Resources) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +/* +func TestContextApply_cancel(t *testing.T) { + stopped := false + stopCh := make(chan struct{}) + stopReplyCh := make(chan struct{}) + + rpAWS := new(MockResourceProvider) + rpAWS.ResourcesReturn = []ResourceType{ + ResourceType{Name: "aws_instance"}, + } + rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + if !stopped { + stopped = true + close(stopCh) + <-stopReplyCh + } + + return &ResourceState{ + ID: "foo", + Attributes: map[string]string{ + "num": "2", + }, + }, nil + } + + c := testConfig(t, "apply-cancel") + tf := testTerraform2(t, &Config{ + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(rpAWS), + }, + }) + + p, err := tf.Plan(&PlanOpts{Config: c}) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Start the Apply in a goroutine + stateCh := make(chan *State) + go func() { + state, err := tf.Apply(p) + if err != nil { + panic(err) + } + + stateCh <- state + }() + + // Start a goroutine so we can inject exactly when we stop + s := tf.stopHook.ref() + go func() { + defer tf.stopHook.unref(s) + <-tf.stopHook.ch + close(stopReplyCh) + tf.stopHook.stoppedCh <- struct{}{} + }() + + <-stopCh + tf.Stop() + + state := <-stateCh + + if len(state.Resources) != 1 { + t.Fatalf("bad: %#v", state.Resources) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyCancelStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} +*/ + +func TestContextApply_compute(t *testing.T) { + c := testConfig(t, "apply-compute") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + ctx.variables = map[string]string{"value": "1"} + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyComputeStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContextApply_destroy(t *testing.T) { + c := testConfig(t, "apply-destroy") + h := new(HookRecordApplyOrder) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + // First plan and apply a create operation + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + // Next, plan and apply a destroy operation + if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil { + t.Fatalf("err: %s", err) + } + + h.Active = true + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test that things were destroyed + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyDestroyStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Test that things were destroyed _in the right order_ + expected2 := []string{"aws_instance.bar", "aws_instance.foo"} + actual2 := h.IDs + if !reflect.DeepEqual(actual2, expected2) { + t.Fatalf("bad: %#v", actual2) + } +} + +func TestContextApply_destroyOrphan(t *testing.T) { + c := testConfig(t, "apply-error") + p := testProvider("aws") + 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, + }) + + p.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + return nil, nil + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(state.Resources) != 0 { + t.Fatalf("bad: %#v", state.Resources) + } +} + +func TestContextApply_error(t *testing.T) { + errored := false + + c := testConfig(t, "apply-error") + p := testProvider("aws") + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + p.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + if errored { + return nil, fmt.Errorf("error") + } + errored = true + + return &ResourceState{ + ID: "foo", + Attributes: map[string]string{ + "num": "2", + }, + }, nil + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err == nil { + t.Fatal("should have error") + } + + if len(state.Resources) != 1 { + t.Fatalf("bad: %#v", state.Resources) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyErrorStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContextApply_errorPartial(t *testing.T) { + errored := false + + c := testConfig(t, "apply-error") + p := testProvider("aws") + s := &State{ + Resources: map[string]*ResourceState{ + "aws_instance.bar": &ResourceState{ + ID: "bar", + Type: "aws_instance", + }, + }, + } + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: s, + }) + + p.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { + if errored { + return nil, fmt.Errorf("error") + } + errored = true + + return &ResourceState{ + ID: "foo", + Attributes: map[string]string{ + "num": "2", + }, + }, nil + } + p.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { + return &ResourceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "num": &ResourceAttrDiff{ + New: "bar", + }, + }, + }, nil + } + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err == nil { + t.Fatal("should have error") + } + + if len(state.Resources) != 2 { + t.Fatalf("bad: %#v", state.Resources) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyErrorPartialStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContextApply_hook(t *testing.T) { + c := testConfig(t, "apply-good") + h := new(MockHook) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + if !h.PreApplyCalled { + t.Fatal("should be called") + } + if !h.PostApplyCalled { + t.Fatal("should be called") + } +} + +func TestContextApply_unknownAttribute(t *testing.T) { + c := testConfig(t, "apply-unknown") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err == nil { + t.Fatal("should error") + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyUnknownAttrStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + +func TestContextApply_vars(t *testing.T) { + c := testConfig(t, "apply-vars") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Config: c, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + Variables: map[string]string{ + "foo": "bar", + }, + }) + + if _, err := ctx.Plan(nil); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyVarsStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } +} + func TestContextPlan(t *testing.T) { c := testConfig(t, "plan-good") p := testProvider("aws") @@ -366,6 +815,37 @@ func testContext(t *testing.T, opts *ContextOpts) *Context { return NewContext(opts) } +func testApplyFn( + s *ResourceState, + d *ResourceDiff) (*ResourceState, error) { + if d.Destroy { + return nil, nil + } + + id := "foo" + if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed { + id = idAttr.New + } + + result := &ResourceState{ + ID: id, + } + + if d != nil { + result = result.MergeDiff(d) + } + + if depAttr, ok := d.Attributes["dep"]; ok { + result.Dependencies = []ResourceDependency{ + ResourceDependency{ + ID: depAttr.New, + }, + } + } + + return result, nil +} + func testDiffFn( s *ResourceState, c *ResourceConfig) (*ResourceDiff, error) { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 8105fabf7..fc899f6db 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -4,7 +4,6 @@ import ( "fmt" "path/filepath" "reflect" - "strings" "sync" "testing" @@ -14,434 +13,6 @@ import ( // This is the directory where our test fixtures are. const fixtureDir = "./test-fixtures" -func TestTerraformApply(t *testing.T) { - c := testConfig(t, "apply-good") - tf := testTerraform2(t, nil) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(state.Resources) < 2 { - t.Fatalf("bad: %#v", state.Resources) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_cancel(t *testing.T) { - stopped := false - stopCh := make(chan struct{}) - stopReplyCh := make(chan struct{}) - - rpAWS := new(MockResourceProvider) - rpAWS.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - } - rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { - return &ResourceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - New: "bar", - }, - }, - }, nil - } - rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { - if !stopped { - stopped = true - close(stopCh) - <-stopReplyCh - } - - return &ResourceState{ - ID: "foo", - Attributes: map[string]string{ - "num": "2", - }, - }, nil - } - - c := testConfig(t, "apply-cancel") - tf := testTerraform2(t, &Config{ - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAWS), - }, - }) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Start the Apply in a goroutine - stateCh := make(chan *State) - go func() { - state, err := tf.Apply(p) - if err != nil { - panic(err) - } - - stateCh <- state - }() - - // Start a goroutine so we can inject exactly when we stop - s := tf.stopHook.ref() - go func() { - defer tf.stopHook.unref(s) - <-tf.stopHook.ch - close(stopReplyCh) - tf.stopHook.stoppedCh <- struct{}{} - }() - - <-stopCh - tf.Stop() - - state := <-stateCh - - if len(state.Resources) != 1 { - t.Fatalf("bad: %#v", state.Resources) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyCancelStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_compute(t *testing.T) { - // This tests that computed variables are properly re-diffed - // to get the value prior to application (Apply). - c := testConfig(t, "apply-compute") - tf := testTerraform2(t, nil) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - p.Vars["value"] = "1" - - state, err := tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyComputeStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_destroy(t *testing.T) { - h := new(HookRecordApplyOrder) - - // First, apply the good configuration, build it - c := testConfig(t, "apply-destroy") - tf := testTerraform2(t, &Config{ - Hooks: []Hook{h}, - }) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Next, plan and apply a destroy operation - p, err = tf.Plan(&PlanOpts{ - Config: new(config.Config), - State: state, - Destroy: true, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - h.Active = true - - state, err = tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Test that things were destroyed - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyDestroyStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } - - // Test that things were destroyed _in the right order_ - expected2 := []string{"aws_instance.bar", "aws_instance.foo"} - actual2 := h.IDs - if !reflect.DeepEqual(actual2, expected2) { - t.Fatalf("bad: %#v", actual2) - } -} - -func TestTerraformApply_destroyOrphan(t *testing.T) { - rpAWS := new(MockResourceProvider) - rpAWS.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - } - rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { - return &ResourceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - New: "bar", - }, - }, - }, nil - } - rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { - return nil, nil - } - - c := testConfig(t, "apply-error") - tf := testTerraform2(t, &Config{ - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAWS), - }, - }) - - s := &State{ - Resources: map[string]*ResourceState{ - "aws_instance.baz": &ResourceState{ - ID: "bar", - Type: "aws_instance", - }, - }, - } - - p, err := tf.Plan(&PlanOpts{Config: c, State: s}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(state.Resources) != 0 { - t.Fatalf("bad: %#v", state.Resources) - } -} - -func TestTerraformApply_error(t *testing.T) { - errored := false - - rpAWS := new(MockResourceProvider) - rpAWS.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - } - rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { - return &ResourceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - New: "bar", - }, - }, - }, nil - } - rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { - if errored { - return nil, fmt.Errorf("error") - } - errored = true - - return &ResourceState{ - ID: "foo", - Attributes: map[string]string{ - "num": "2", - }, - }, nil - } - - c := testConfig(t, "apply-error") - tf := testTerraform2(t, &Config{ - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAWS), - }, - }) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err == nil { - t.Fatal("should have error") - } - - if len(state.Resources) != 1 { - t.Fatalf("bad: %#v", state.Resources) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyErrorStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_errorPartial(t *testing.T) { - errored := false - - rpAWS := new(MockResourceProvider) - rpAWS.ResourcesReturn = []ResourceType{ - ResourceType{Name: "aws_instance"}, - } - rpAWS.DiffFn = func(*ResourceState, *ResourceConfig) (*ResourceDiff, error) { - return &ResourceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - New: "bar", - }, - }, - }, nil - } - rpAWS.ApplyFn = func(*ResourceState, *ResourceDiff) (*ResourceState, error) { - if errored { - return nil, fmt.Errorf("error") - } - errored = true - - return &ResourceState{ - ID: "foo", - Attributes: map[string]string{ - "num": "2", - }, - }, nil - } - - c := testConfig(t, "apply-error") - tf := testTerraform2(t, &Config{ - Providers: map[string]ResourceProviderFactory{ - "aws": testProviderFuncFixed(rpAWS), - }, - }) - - s := &State{ - Resources: map[string]*ResourceState{ - "aws_instance.bar": &ResourceState{ - ID: "bar", - Type: "aws_instance", - }, - }, - } - - p, err := tf.Plan(&PlanOpts{Config: c, State: s}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err == nil { - t.Fatal("should have error") - } - - if len(state.Resources) != 2 { - t.Fatalf("bad: %#v", state.Resources) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyErrorPartialStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_hook(t *testing.T) { - c := testConfig(t, "apply-good") - h := new(MockHook) - tf := testTerraform2(t, &Config{ - Hooks: []Hook{h}, - }) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - if _, err := tf.Apply(p); err != nil { - t.Fatalf("err: %s", err) - } - - if !h.PreApplyCalled { - t.Fatal("should be called") - } - if !h.PostApplyCalled { - t.Fatal("should be called") - } -} - -func TestTerraformApply_unknownAttribute(t *testing.T) { - c := testConfig(t, "apply-unknown") - tf := testTerraform2(t, nil) - - p, err := tf.Plan(&PlanOpts{Config: c}) - if err != nil { - t.Fatalf("err: %s", err) - } - - state, err := tf.Apply(p) - if err == nil { - t.Fatal("should error") - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyUnknownAttrStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - -func TestTerraformApply_vars(t *testing.T) { - c := testConfig(t, "apply-vars") - tf := testTerraform2(t, nil) - - p, err := tf.Plan(&PlanOpts{ - Config: c, - Vars: map[string]string{"foo": "baz"}, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Explicitly set the "foo" variable - p.Vars["foo"] = "bar" - - state, err := tf.Apply(p) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyVarsStr) - if actual != expected { - t.Fatalf("bad: \n%s", actual) - } -} - func TestTerraformRefresh(t *testing.T) { rpAWS := new(MockResourceProvider) rpAWS.ResourcesReturn = []ResourceType{