// Context returns a Terraform Context taking into account the context // options used to initialize this meta configuration. func (m *Meta) Context(path, statePath string) (*terraform.Context, bool, error) { opts := m.contextOpts() // First try to just read the plan directly from the path given. f, err := os.Open(path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { if len(m.variables) > 0 { return nil, false, fmt.Errorf( "You can't set variables with the '-var' or '-var-file' flag\n" + "when you're applying a plan file. The variables used when\n" + "the plan was created will be used. If you wish to use different\n" + "variable values, create a new plan file.") } return plan.Context(opts), true, nil } } // Load up the state var state *terraform.State if statePath != "" { f, err := os.Open(statePath) if err != nil && os.IsNotExist(err) { // If the state file doesn't exist, it is okay, since it // is probably a new infrastructure. err = nil } else if err == nil { state, err = terraform.ReadState(f) f.Close() } if err != nil { return nil, false, fmt.Errorf("Error loading state: %s", err) } } // Store the loaded state m.state = state config, err := config.LoadDir(path) if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) } if err := config.Validate(); err != nil { return nil, false, fmt.Errorf("Error validating config: %s", err) } opts.Config = config opts.State = state ctx := terraform.NewContext(opts) return ctx, false, nil }
// Context returns a Terraform Context taking into account the context // options used to initialize this meta configuration. func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { opts := m.contextOpts() // First try to just read the plan directly from the path given. f, err := os.Open(copts.Path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { if len(m.variables) > 0 { return nil, false, fmt.Errorf( "You can't set variables with the '-var' or '-var-file' flag\n" + "when you're applying a plan file. The variables used when\n" + "the plan was created will be used. If you wish to use different\n" + "variable values, create a new plan file.") } return plan.Context(opts), true, nil } } // Load the statePath if not given if copts.StatePath != "" { m.statePath = copts.StatePath } // Store the loaded state state, err := m.loadState() if err != nil { return nil, false, err } m.state = state // Load the root module mod, err := module.NewTreeModule("", copts.Path) if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) } dataDir := DefaultDataDirectory if m.dataDir != "" { dataDir = m.dataDir } err = mod.Load(m.moduleStorage(dataDir), copts.GetMode) if err != nil { return nil, false, fmt.Errorf("Error downloading modules: %s", err) } opts.Module = mod opts.State = state ctx := terraform.NewContext(opts) return ctx, false, nil }
func testSession(t *testing.T, test testSessionTest) { // Build the TF context ctx, err := terraform.NewContext(&terraform.ContextOpts{ State: test.State, Module: module.NewEmptyTree(), }) if err != nil { t.Fatalf("err: %s", err) } // Build the session s := &Session{ Interpolater: ctx.Interpolater(), } // Test the inputs. We purposely don't use subtests here because // the inputs don't recognize subtests, but a sequence of stateful // operations. for _, input := range test.Inputs { result, err := s.Handle(input.Input) if (err != nil) != input.Error { t.Fatalf("%q: err: %s", input.Input, err) } if err != nil { if input.ErrorContains != "" { if !strings.Contains(err.Error(), input.ErrorContains) { t.Fatalf( "%q: err should contain: %q\n\n%s", input.Input, input.ErrorContains, err) } } continue } if input.Output != "" && result != input.Output { t.Fatalf( "%q: expected:\n\n%s\n\ngot:\n\n%s", input.Input, input.Output, result) } if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) { t.Fatalf( "%q: expected contains:\n\n%s\n\ngot:\n\n%s", input.Input, input.OutputContains, result) } } }
// Context returns a Terraform Context taking into account the context // options used to initialize this meta configuration. func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { opts := m.contextOpts() // First try to just read the plan directly from the path given. f, err := os.Open(copts.Path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { // Setup our state state, statePath, err := StateFromPlan(m.statePath, m.stateOutPath, plan) if err != nil { return nil, false, fmt.Errorf("Error loading plan: %s", err) } // Set our state m.state = state // this is used for printing the saved location later if m.stateOutPath == "" { m.stateOutPath = statePath } if len(m.variables) > 0 { return nil, false, fmt.Errorf( "You can't set variables with the '-var' or '-var-file' flag\n" + "when you're applying a plan file. The variables used when\n" + "the plan was created will be used. If you wish to use different\n" + "variable values, create a new plan file.") } ctx, err := plan.Context(opts) return ctx, true, err } } // Load the statePath if not given if copts.StatePath != "" { m.statePath = copts.StatePath } // Tell the context if we're in a destroy plan / apply opts.Destroy = copts.Destroy // Store the loaded state state, err := m.State() if err != nil { return nil, false, err } // Load the root module var mod *module.Tree if copts.Path != "" { mod, err = module.NewTreeModule("", copts.Path) if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) } } else { mod = module.NewEmptyTree() } err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode) if err != nil { return nil, false, fmt.Errorf("Error downloading modules: %s", err) } opts.Module = mod opts.Parallelism = copts.Parallelism opts.State = state.State() ctx, err := terraform.NewContext(opts) return ctx, false, err }
func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { mod, err := testModule(opts, step) if err != nil { return state, err } // Build the context opts.Module = mod opts.State = state opts.Destroy = step.Destroy ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { if len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return state, fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } log.Printf("[WARN] Config warnings: %#v", ws) } // Refresh! state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error refreshing: %s", err) } // Plan! if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { log.Printf("[WARN] Test: Step plan: %s", p) } // We need to keep a copy of the state prior to destroying // such that destroy steps can verify their behaviour in the check // function stateBeforeApplication := state.DeepCopy() // Apply! state, err = ctx.Apply() if err != nil { return state, fmt.Errorf("Error applying: %s", err) } // Check! Excitement! if step.Check != nil { if step.Destroy { if err := step.Check(stateBeforeApplication); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } else { if err := step.Check(state); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } } // Now, verify that Plan is now empty and we don't have a perpetual diff issue // We do this with TWO plans. One without a refresh. var p *terraform.Plan if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on follow-up plan: %s", err) } if p.Diff != nil && !p.Diff.Empty() { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step, the plan was not empty:\n\n%s", p) } } // And another after a Refresh. state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error on follow-up refresh: %s", err) } if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on second follow-up plan: %s", err) } if p.Diff != nil && !p.Diff.Empty() { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step and refreshing, "+ "the plan was not empty:\n\n%s", p) } } // Made it here, but expected a non-empty plan, fail! if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") } // Made it here? Good job test step! return state, nil }
func testIDOnlyRefresh(c TestCase, opts terraform.ContextOpts, step TestStep, r *terraform.ResourceState) error { // TODO: We guard by this right now so master doesn't explode. We // need to remove this eventually to make this part of the normal tests. if os.Getenv("TF_ACC_IDONLY") == "" { return nil } name := fmt.Sprintf("%s.foo", r.Type) // Build the state. The state is just the resource with an ID. There // are no attributes. We only set what is needed to perform a refresh. state := terraform.NewState() state.RootModule().Resources[name] = &terraform.ResourceState{ Type: r.Type, Primary: &terraform.InstanceState{ ID: r.Primary.ID, }, } // Create the config module. We use the full config because Refresh // doesn't have access to it and we may need things like provider // configurations. The initial implementation of id-only checks used // an empty config module, but that caused the aforementioned problems. mod, err := testModule(opts, step) if err != nil { return err } // Initialize the context opts.Module = mod opts.State = state ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { if len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } log.Printf("[WARN] Config warnings: %#v", ws) } // Refresh! state, err = ctx.Refresh() if err != nil { return fmt.Errorf("Error refreshing: %s", err) } // Verify attribute equivalence. actualR := state.RootModule().Resources[name] if actualR == nil { return fmt.Errorf("Resource gone!") } if actualR.Primary == nil { return fmt.Errorf("Resource has no primary instance") } actual := actualR.Primary.Attributes expected := r.Primary.Attributes // Remove fields we're ignoring for _, v := range c.IDRefreshIgnore { delete(actual, v) delete(expected, v) } if !reflect.DeepEqual(actual, expected) { // Determine only the different attributes for k, v := range expected { if av, ok := actual[k]; ok && v == av { delete(expected, k) delete(actual, k) } } spewConf := spew.NewDefaultConfig() spewConf.SortKeys = true return fmt.Errorf( "Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ "\n\n%s\n\n%s", spewConf.Sdump(actual), spewConf.Sdump(expected)) } return nil }
// testStepImportState runs an imort state test step func testStepImportState( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { // Determine the ID to import importId := step.ImportStateId if importId == "" { resource, err := testResource(step, state) if err != nil { return state, err } importId = resource.Primary.ID } // Setup the context. We initialize with an empty state. We use the // full config for provider configurations. mod, err := testModule(opts, step) if err != nil { return state, err } opts.Module = mod opts.State = terraform.NewState() ctx, err := terraform.NewContext(&opts) if err != nil { return state, err } // Do the import! newState, err := ctx.Import(&terraform.ImportOpts{ // Set the module so that any provider config is loaded Module: mod, Targets: []*terraform.ImportTarget{ &terraform.ImportTarget{ Addr: step.ResourceName, ID: importId, }, }, }) if err != nil { log.Printf("[ERROR] Test: ImportState failure: %s", err) return state, err } // Go through the new state and verify if step.ImportStateCheck != nil { var states []*terraform.InstanceState for _, r := range newState.RootModule().Resources { if r.Primary != nil { states = append(states, r.Primary) } } if err := step.ImportStateCheck(states); err != nil { return state, err } } // Verify that all the states match if step.ImportStateVerify { new := newState.RootModule().Resources old := state.RootModule().Resources for _, r := range new { // Find the existing resource var oldR *terraform.ResourceState for _, r2 := range old { if r2.Primary != nil && r2.Primary.ID == r.Primary.ID { oldR = r2 break } } if oldR == nil { return state, fmt.Errorf( "Failed state verification, resource with ID %s not found", r.Primary.ID) } // Compare their attributes actual := make(map[string]string) for k, v := range r.Primary.Attributes { actual[k] = v } expected := make(map[string]string) for k, v := range oldR.Primary.Attributes { expected[k] = v } // Remove fields we're ignoring for _, v := range step.ImportStateVerifyIgnore { for k, _ := range actual { if strings.HasPrefix(k, v) { delete(actual, k) } } for k, _ := range expected { if strings.HasPrefix(k, v) { delete(expected, k) } } } if !reflect.DeepEqual(actual, expected) { // Determine only the different attributes for k, v := range expected { if av, ok := actual[k]; ok && v == av { delete(expected, k) delete(actual, k) } } spewConf := spew.NewDefaultConfig() spewConf.SortKeys = true return state, fmt.Errorf( "ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ "\n\n%s\n\n%s", spewConf.Sdump(actual), spewConf.Sdump(expected)) } } } // Return the old state (non-imported) so we don't change anything. return state, nil }
func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { // Write the configuration cfgF, err := ioutil.TempFile("", "tf-test") if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } cfgPath := cfgF.Name() + ".tf" cfgF.Close() os.Remove(cfgF.Name()) cfgF, err = os.Create(cfgPath) if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } defer os.Remove(cfgPath) _, err = io.Copy(cfgF, strings.NewReader(step.Config)) cfgF.Close() if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } // Parse the configuration config, err := config.Load(cfgPath) if err != nil { return state, fmt.Errorf( "Error parsing configuration: %s", err) } // Build the context opts.Config = config opts.State = state ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return state, fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } // Refresh! state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error refreshing: %s", err) } // Plan! if p, err := ctx.Plan(&terraform.PlanOpts{Destroy: step.Destroy}); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { log.Printf("[WARN] Test: Step plan: %s", p) } // Apply! state, err = ctx.Apply() if err != nil { return state, fmt.Errorf("Error applying: %s", err) } // Check! Excitement! if step.Check != nil { if err = step.Check(state); err != nil { err = fmt.Errorf("Check failed: %s", err) } } return state, err }
func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { cfgPath, err := ioutil.TempDir("", "tf-test") if err != nil { return state, fmt.Errorf( "Error creating temporary directory for config: %s", err) } defer os.RemoveAll(cfgPath) // Write the configuration cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf")) if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } _, err = io.Copy(cfgF, strings.NewReader(step.Config)) cfgF.Close() if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } // Parse the configuration mod, err := module.NewTreeModule("", cfgPath) if err != nil { return state, fmt.Errorf( "Error loading configuration: %s", err) } // Load the modules modStorage := &module.FolderStorage{ StorageDir: filepath.Join(cfgPath, ".tfmodules"), } err = mod.Load(modStorage, module.GetModeGet) if err != nil { return state, fmt.Errorf("Error downloading modules: %s", err) } // Build the context opts.Module = mod opts.State = state opts.Destroy = step.Destroy ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return state, fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } // Refresh! state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error refreshing: %s", err) } // Plan! if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { log.Printf("[WARN] Test: Step plan: %s", p) } // Apply! state, err = ctx.Apply() if err != nil { return state, fmt.Errorf("Error applying: %s", err) } // Check! Excitement! if step.Check != nil { if err = step.Check(state); err != nil { err = fmt.Errorf("Check failed: %s", err) } } return state, err }
// Context returns a Terraform Context taking into account the context // options used to initialize this meta configuration. func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { opts := m.contextOpts() // First try to just read the plan directly from the path given. f, err := os.Open(copts.Path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { // Setup our state, force it to use our plan's state stateOpts := m.StateOpts() if plan != nil { stateOpts.ForceState = plan.State } // Get the state result, err := State(stateOpts) if err != nil { return nil, false, fmt.Errorf("Error loading plan: %s", err) } // Set our state m.state = result.State // this is used for printing the saved location later if m.stateOutPath == "" { m.stateOutPath = result.StatePath } if len(m.variables) > 0 { return nil, false, fmt.Errorf( "You can't set variables with the '-var' or '-var-file' flag\n" + "when you're applying a plan file. The variables used when\n" + "the plan was created will be used. If you wish to use different\n" + "variable values, create a new plan file.") } ctx, err := plan.Context(opts) return ctx, true, err } } // Load the statePath if not given if copts.StatePath != "" { m.statePath = copts.StatePath } // Tell the context if we're in a destroy plan / apply opts.Destroy = copts.Destroy // Store the loaded state state, err := m.State() if err != nil { return nil, false, err } // Load the root module var mod *module.Tree if copts.Path != "" { mod, err = module.NewTreeModule("", copts.Path) // Check for the error where we have no config files but // allow that. If that happens, clear the error. if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) && copts.PathEmptyOk { log.Printf( "[WARN] Empty configuration dir, ignoring: %s", copts.Path) err = nil mod = module.NewEmptyTree() } if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) } } else { mod = module.NewEmptyTree() } err = mod.Load(m.moduleStorage(m.DataDir()), copts.GetMode) if err != nil { return nil, false, fmt.Errorf("Error downloading modules: %s", err) } // Validate the module right away if err := mod.Validate(); err != nil { return nil, false, err } opts.Module = mod opts.Parallelism = copts.Parallelism opts.State = state.State() ctx, err := terraform.NewContext(opts) return ctx, false, err }
func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { mod, err := testModule(opts, step) if err != nil { return state, err } // Build the context opts.Module = mod opts.State = state opts.Destroy = step.Destroy ctx, err := terraform.NewContext(&opts) if err != nil { return state, fmt.Errorf("Error initializing context: %s", err) } if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { if len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return state, fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } log.Printf("[WARN] Config warnings: %#v", ws) } // Refresh! state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error refreshing: %s", err) } // Plan! if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { log.Printf("[WARN] Test: Step plan: %s", p) } // We need to keep a copy of the state prior to destroying // such that destroy steps can verify their behaviour in the check // function stateBeforeApplication := state.DeepCopy() // Apply! state, err = ctx.Apply() if err != nil { return state, fmt.Errorf("Error applying: %s", err) } // Check! Excitement! if step.Check != nil { if step.Destroy { if err := step.Check(stateBeforeApplication); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } else { if err := step.Check(state); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } } // Now, verify that Plan is now empty and we don't have a perpetual diff issue // We do this with TWO plans. One without a refresh. var p *terraform.Plan if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on follow-up plan: %s", err) } if p.Diff != nil && !p.Diff.Empty() { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step, the plan was not empty:\n\n%s", p) } } // And another after a Refresh. state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error on follow-up refresh: %s", err) } if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on second follow-up plan: %s", err) } empty := p.Diff == nil || p.Diff.Empty() // Data resources are tricky because they legitimately get instantiated // during refresh so that they will be already populated during the // plan walk. Because of this, if we have any data resources in the // config we'll end up wanting to destroy them again here. This is // acceptable and expected, and we'll treat it as "empty" for the // sake of this testing. if step.Destroy { empty = true for _, moduleDiff := range p.Diff.Modules { for k, instanceDiff := range moduleDiff.Resources { if !strings.HasPrefix(k, "data.") { empty = false break } if !instanceDiff.Destroy { empty = false } } } } if !empty { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step and refreshing, "+ "the plan was not empty:\n\n%s", p) } } // Made it here, but expected a non-empty plan, fail! if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") } // Made it here? Good job test step! return state, nil }
func testStep( opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { if step.PreConfig != nil { step.PreConfig() } cfgPath, err := ioutil.TempDir("", "tf-test") if err != nil { return state, fmt.Errorf( "Error creating temporary directory for config: %s", err) } defer os.RemoveAll(cfgPath) // Write the configuration cfgF, err := os.Create(filepath.Join(cfgPath, "main.tf")) if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } _, err = io.Copy(cfgF, strings.NewReader(step.Config)) cfgF.Close() if err != nil { return state, fmt.Errorf( "Error creating temporary file for config: %s", err) } // Parse the configuration mod, err := module.NewTreeModule("", cfgPath) if err != nil { return state, fmt.Errorf( "Error loading configuration: %s", err) } // Load the modules modStorage := &getter.FolderStorage{ StorageDir: filepath.Join(cfgPath, ".tfmodules"), } err = mod.Load(modStorage, module.GetModeGet) if err != nil { return state, fmt.Errorf("Error downloading modules: %s", err) } // Build the context opts.Module = mod opts.State = state opts.Destroy = step.Destroy ctx := terraform.NewContext(&opts) if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { if len(es) > 0 { estrs := make([]string, len(es)) for i, e := range es { estrs[i] = e.Error() } return state, fmt.Errorf( "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", ws, estrs) } log.Printf("[WARN] Config warnings: %#v", ws) } // Refresh! state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error refreshing: %s", err) } // Plan! if p, err := ctx.Plan(); err != nil { return state, fmt.Errorf( "Error planning: %s", err) } else { log.Printf("[WARN] Test: Step plan: %s", p) } // We need to keep a copy of the state prior to destroying // such that destroy steps can verify their behaviour in the check // function stateBeforeApplication := state.DeepCopy() // Apply! state, err = ctx.Apply() if err != nil { return state, fmt.Errorf("Error applying: %s", err) } // Check! Excitement! if step.Check != nil { if step.Destroy { if err := step.Check(stateBeforeApplication); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } else { if err := step.Check(state); err != nil { return state, fmt.Errorf("Check failed: %s", err) } } } // Now, verify that Plan is now empty and we don't have a perpetual diff issue // We do this with TWO plans. One without a refresh. var p *terraform.Plan if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on follow-up plan: %s", err) } if p.Diff != nil && !p.Diff.Empty() { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step, the plan was not empty:\n\n%s", p) } } // And another after a Refresh. state, err = ctx.Refresh() if err != nil { return state, fmt.Errorf( "Error on follow-up refresh: %s", err) } if p, err = ctx.Plan(); err != nil { return state, fmt.Errorf("Error on second follow-up plan: %s", err) } if p.Diff != nil && !p.Diff.Empty() { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) } else { return state, fmt.Errorf( "After applying this step and refreshing, "+ "the plan was not empty:\n\n%s", p) } } // Made it here, but expected a non-empty plan, fail! if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") } // Made it here? Good job test step! return state, nil }