backend/local
The local backend implementation is an implementation of backend.Enhanced that recreates all the behavior of the CLI but through the backend interface.
This commit is contained in:
parent
8a070ddef0
commit
397e1b3132
|
@ -0,0 +1,211 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
)
|
||||
|
||||
// Local is an implementation of EnhancedBackend that performs all operations
|
||||
// locally. This is the "default" backend and implements normal Terraform
|
||||
// behavior as it is well known.
|
||||
type Local struct {
|
||||
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
|
||||
// output will be done. If CLIColor is nil then no coloring will be done.
|
||||
CLI cli.Ui
|
||||
CLIColor *colorstring.Colorize
|
||||
|
||||
// StatePath is the local path where state is read from.
|
||||
//
|
||||
// StateOutPath is the local path where the state will be written.
|
||||
// If this is empty, it will default to StatePath.
|
||||
//
|
||||
// StateBackupPath is the local path where a backup file will be written.
|
||||
// If this is empty, no backup will be taken.
|
||||
StatePath string
|
||||
StateOutPath string
|
||||
StateBackupPath string
|
||||
|
||||
// ContextOpts are the base context options to set when initializing a
|
||||
// Terraform context. Many of these will be overridden or merged by
|
||||
// Operation. See Operation for more details.
|
||||
ContextOpts *terraform.ContextOpts
|
||||
|
||||
// OpInput will ask for necessary input prior to performing any operations.
|
||||
//
|
||||
// OpValidation will perform validation prior to running an operation. The
|
||||
// variable naming doesn't match the style of others since we have a func
|
||||
// Validate.
|
||||
OpInput bool
|
||||
OpValidation bool
|
||||
|
||||
// Backend, if non-nil, will use this backend for non-enhanced behavior.
|
||||
// This allows local behavior with remote state storage. It is a way to
|
||||
// "upgrade" a non-enhanced backend to an enhanced backend with typical
|
||||
// behavior.
|
||||
//
|
||||
// If this is nil, local performs normal state loading and storage.
|
||||
Backend backend.Backend
|
||||
|
||||
schema *schema.Backend
|
||||
opLock sync.Mutex
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (b *Local) Input(
|
||||
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
|
||||
b.once.Do(b.init)
|
||||
|
||||
f := b.schema.Input
|
||||
if b.Backend != nil {
|
||||
f = b.Backend.Input
|
||||
}
|
||||
|
||||
return f(ui, c)
|
||||
}
|
||||
|
||||
func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
|
||||
b.once.Do(b.init)
|
||||
|
||||
f := b.schema.Validate
|
||||
if b.Backend != nil {
|
||||
f = b.Backend.Validate
|
||||
}
|
||||
|
||||
return f(c)
|
||||
}
|
||||
|
||||
func (b *Local) Configure(c *terraform.ResourceConfig) error {
|
||||
b.once.Do(b.init)
|
||||
|
||||
f := b.schema.Configure
|
||||
if b.Backend != nil {
|
||||
f = b.Backend.Configure
|
||||
}
|
||||
|
||||
return f(c)
|
||||
}
|
||||
|
||||
func (b *Local) State() (state.State, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.State()
|
||||
}
|
||||
|
||||
// Otherwise, we need to load the state.
|
||||
var s state.State = &state.LocalState{
|
||||
Path: b.StatePath,
|
||||
PathOut: b.StateOutPath,
|
||||
}
|
||||
|
||||
// Load the state as a sanity check
|
||||
if err := s.RefreshState(); err != nil {
|
||||
return nil, errwrap.Wrapf("Error reading local state: {{err}}", err)
|
||||
}
|
||||
|
||||
// If we are backing up the state, wrap it
|
||||
if path := b.StateBackupPath; path != "" {
|
||||
s = &state.BackupState{
|
||||
Real: s,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Operation implements backend.Enhanced
|
||||
//
|
||||
// This will initialize an in-memory terraform.Context to perform the
|
||||
// operation within this process.
|
||||
//
|
||||
// The given operation parameter will be merged with the ContextOpts on
|
||||
// the structure with the following rules. If a rule isn't specified and the
|
||||
// name conflicts, assume that the field is overwritten if set.
|
||||
func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
||||
// Determine the function to call for our operation
|
||||
var f func(context.Context, *backend.Operation, *backend.RunningOperation)
|
||||
switch op.Type {
|
||||
case backend.OperationTypeRefresh:
|
||||
f = b.opRefresh
|
||||
case backend.OperationTypePlan:
|
||||
f = b.opPlan
|
||||
case backend.OperationTypeApply:
|
||||
f = b.opApply
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"Unsupported operation type: %s\n\n"+
|
||||
"This is a bug in Terraform and should be reported. The local backend\n"+
|
||||
"is built-in to Terraform and should always support all operations.",
|
||||
op.Type)
|
||||
}
|
||||
|
||||
// Lock
|
||||
b.opLock.Lock()
|
||||
|
||||
// Build our running operation
|
||||
runningCtx, runningCtxCancel := context.WithCancel(context.Background())
|
||||
runningOp := &backend.RunningOperation{Context: runningCtx}
|
||||
|
||||
// Do it
|
||||
go func() {
|
||||
defer b.opLock.Unlock()
|
||||
defer runningCtxCancel()
|
||||
f(ctx, op, runningOp)
|
||||
}()
|
||||
|
||||
// Return
|
||||
return runningOp, nil
|
||||
}
|
||||
|
||||
// Colorize returns the Colorize structure that can be used for colorizing
|
||||
// output. This is gauranteed to always return a non-nil value and so is useful
|
||||
// as a helper to wrap any potentially colored strings.
|
||||
func (b *Local) Colorize() *colorstring.Colorize {
|
||||
if b.CLIColor != nil {
|
||||
return b.CLIColor
|
||||
}
|
||||
|
||||
return &colorstring.Colorize{
|
||||
Colors: colorstring.DefaultColors,
|
||||
Disable: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Local) init() {
|
||||
b.schema = &schema.Backend{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"path": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
|
||||
ConfigureFunc: b.schemaConfigure,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Local) schemaConfigure(ctx context.Context) error {
|
||||
d := schema.FromContextBackendConfig(ctx)
|
||||
|
||||
// Set the path if it is set
|
||||
pathRaw, ok := d.GetOk("path")
|
||||
if ok {
|
||||
path := pathRaw.(string)
|
||||
if path == "" {
|
||||
return fmt.Errorf("configured path is empty")
|
||||
}
|
||||
|
||||
b.StatePath = path
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func (b *Local) opApply(
|
||||
ctx context.Context,
|
||||
op *backend.Operation,
|
||||
runningOp *backend.RunningOperation) {
|
||||
log.Printf("[INFO] backend/local: starting Apply operation")
|
||||
|
||||
// Setup our count hook that keeps track of resource changes
|
||||
countHook := new(CountHook)
|
||||
stateHook := new(StateHook)
|
||||
if b.ContextOpts == nil {
|
||||
b.ContextOpts = new(terraform.ContextOpts)
|
||||
}
|
||||
old := b.ContextOpts.Hooks
|
||||
defer func() { b.ContextOpts.Hooks = old }()
|
||||
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook, stateHook)
|
||||
|
||||
// Get our context
|
||||
tfCtx, state, err := b.context(op)
|
||||
if err != nil {
|
||||
runningOp.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
// Setup the state
|
||||
runningOp.State = tfCtx.State()
|
||||
|
||||
// If we weren't given a plan, then we refresh/plan
|
||||
if op.Plan == nil {
|
||||
// If we're refreshing before apply, perform that
|
||||
if op.PlanRefresh {
|
||||
log.Printf("[INFO] backend/local: apply calling Refresh")
|
||||
_, err := tfCtx.Refresh()
|
||||
if err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the plan
|
||||
log.Printf("[INFO] backend/local: apply calling Plan")
|
||||
if _, err := tfCtx.Plan(); err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Setup our hook for continuous state updates
|
||||
stateHook.State = state
|
||||
|
||||
// Start the apply in a goroutine so that we can be interrupted.
|
||||
var applyState *terraform.State
|
||||
var applyErr error
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
applyState, applyErr = tfCtx.Apply()
|
||||
|
||||
/*
|
||||
// Record any shadow errors for later
|
||||
if err := ctx.ShadowError(); err != nil {
|
||||
shadowErr = multierror.Append(shadowErr, multierror.Prefix(
|
||||
err, "apply operation:"))
|
||||
}
|
||||
*/
|
||||
}()
|
||||
|
||||
// Wait for the apply to finish or for us to be interrupted so
|
||||
// we can handle it properly.
|
||||
err = nil
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if b.CLI != nil {
|
||||
b.CLI.Output("Interrupt received. Gracefully shutting down...")
|
||||
}
|
||||
|
||||
// Stop execution
|
||||
go tfCtx.Stop()
|
||||
|
||||
// Wait for completion still
|
||||
<-doneCh
|
||||
case <-doneCh:
|
||||
}
|
||||
|
||||
// Store the final state
|
||||
runningOp.State = applyState
|
||||
|
||||
// Persist the state
|
||||
if err := state.WriteState(applyState); err != nil {
|
||||
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
||||
return
|
||||
}
|
||||
if err := state.PersistState(); err != nil {
|
||||
runningOp.Err = fmt.Errorf("Failed to save state: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if applyErr != nil {
|
||||
runningOp.Err = fmt.Errorf(
|
||||
"Error applying plan:\n\n"+
|
||||
"%s\n\n"+
|
||||
"Terraform does not automatically rollback in the face of errors.\n"+
|
||||
"Instead, your Terraform state file has been partially updated with\n"+
|
||||
"any resources that successfully completed. Please address the error\n"+
|
||||
"above and apply again to incrementally change your infrastructure.",
|
||||
multierror.Flatten(applyErr))
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a UI, output the results
|
||||
if b.CLI != nil {
|
||||
if op.Destroy {
|
||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
||||
"[reset][bold][green]\n"+
|
||||
"Destroy complete! Resources: %d destroyed.",
|
||||
countHook.Removed)))
|
||||
} else {
|
||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
||||
"[reset][bold][green]\n"+
|
||||
"Apply complete! Resources: %d added, %d changed, %d destroyed.",
|
||||
countHook.Added,
|
||||
countHook.Changed,
|
||||
countHook.Removed)))
|
||||
}
|
||||
|
||||
if countHook.Added > 0 || countHook.Changed > 0 {
|
||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
||||
"[reset]\n"+
|
||||
"The state of your infrastructure has been saved to the path\n"+
|
||||
"below. This state is required to modify and destroy your\n"+
|
||||
"infrastructure, so keep it safe. To inspect the complete state\n"+
|
||||
"use the `terraform show` command.\n\n"+
|
||||
"State path: %s",
|
||||
b.StateOutPath)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestLocal_applyBasic(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
|
||||
p.ApplyReturn = &terraform.InstanceState{ID: "yes"}
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply")
|
||||
defer modCleanup()
|
||||
|
||||
op := testOperationApply()
|
||||
op.Module = mod
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.RefreshCalled {
|
||||
t.Fatal("refresh should not be called")
|
||||
}
|
||||
|
||||
if !p.DiffCalled {
|
||||
t.Fatal("diff should be called")
|
||||
}
|
||||
|
||||
if !p.ApplyCalled {
|
||||
t.Fatal("apply should be called")
|
||||
}
|
||||
|
||||
checkState(t, b.StateOutPath, `
|
||||
test_instance.foo:
|
||||
ID = yes
|
||||
`)
|
||||
}
|
||||
|
||||
func TestLocal_applyError(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
|
||||
var lock sync.Mutex
|
||||
errored := false
|
||||
p.ApplyFn = func(
|
||||
info *terraform.InstanceInfo,
|
||||
s *terraform.InstanceState,
|
||||
d *terraform.InstanceDiff) (*terraform.InstanceState, error) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
if !errored && info.Id == "test_instance.bar" {
|
||||
errored = true
|
||||
return nil, fmt.Errorf("error")
|
||||
}
|
||||
|
||||
return &terraform.InstanceState{ID: "foo"}, nil
|
||||
}
|
||||
p.DiffFn = func(
|
||||
*terraform.InstanceInfo,
|
||||
*terraform.InstanceState,
|
||||
*terraform.ResourceConfig) (*terraform.InstanceDiff, error) {
|
||||
return &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"ami": &terraform.ResourceAttrDiff{
|
||||
New: "bar",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-error")
|
||||
defer modCleanup()
|
||||
|
||||
op := testOperationApply()
|
||||
op.Module = mod
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
checkState(t, b.StateOutPath, `
|
||||
test_instance.foo:
|
||||
ID = foo
|
||||
`)
|
||||
}
|
||||
|
||||
func testOperationApply() *backend.Operation {
|
||||
return &backend.Operation{
|
||||
Type: backend.OperationTypeApply,
|
||||
}
|
||||
}
|
||||
|
||||
// testApplyState is just a common state that we use for testing refresh.
|
||||
func testApplyState() *terraform.State {
|
||||
return &terraform.State{
|
||||
Version: 2,
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// backend.Local implementation.
|
||||
func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, error) {
|
||||
// Make sure the type is invalid. We use this as a way to know not
|
||||
// to ask for input/validate.
|
||||
op.Type = backend.OperationTypeInvalid
|
||||
|
||||
return b.context(op)
|
||||
}
|
||||
|
||||
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
|
||||
// Get the state.
|
||||
s, err := b.State()
|
||||
if err != nil {
|
||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
||||
}
|
||||
if err := s.RefreshState(); err != nil {
|
||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
||||
}
|
||||
|
||||
// Initialize our context options
|
||||
var opts terraform.ContextOpts
|
||||
if v := b.ContextOpts; v != nil {
|
||||
opts = *v
|
||||
}
|
||||
|
||||
// Copy set options from the operation
|
||||
opts.Destroy = op.Destroy
|
||||
opts.Module = op.Module
|
||||
opts.Targets = op.Targets
|
||||
opts.UIInput = op.UIIn
|
||||
if op.Variables != nil {
|
||||
opts.Variables = op.Variables
|
||||
}
|
||||
|
||||
// Load our state
|
||||
opts.State = s.State()
|
||||
|
||||
// Build the context
|
||||
var tfCtx *terraform.Context
|
||||
if op.Plan != nil {
|
||||
tfCtx, err = op.Plan.Context(&opts)
|
||||
} else {
|
||||
tfCtx, err = terraform.NewContext(&opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// If we have an operation, then we automatically do the input/validate
|
||||
// here since every option requires this.
|
||||
if op.Type != backend.OperationTypeInvalid {
|
||||
// If input asking is enabled, then do that
|
||||
if op.Plan == nil && b.OpInput {
|
||||
mode := terraform.InputModeProvider
|
||||
mode |= terraform.InputModeVar
|
||||
mode |= terraform.InputModeVarUnset
|
||||
|
||||
if err := tfCtx.Input(mode); err != nil {
|
||||
return nil, nil, errwrap.Wrapf("Error asking for user input: {{err}}", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If validation is enabled, validate
|
||||
if b.OpValidation {
|
||||
// We ignore warnings here on purpose. We expect users to be listening
|
||||
// to the terraform.Hook called after a validation.
|
||||
_, es := tfCtx.Validate()
|
||||
if len(es) > 0 {
|
||||
return nil, nil, multierror.Append(nil, es...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tfCtx, s, nil
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/command/format"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func (b *Local) opPlan(
|
||||
ctx context.Context,
|
||||
op *backend.Operation,
|
||||
runningOp *backend.RunningOperation) {
|
||||
log.Printf("[INFO] backend/local: starting Plan operation")
|
||||
|
||||
if b.CLI != nil && op.Plan != nil {
|
||||
b.CLI.Output(b.Colorize().Color(
|
||||
"[reset][bold][yellow]" +
|
||||
"The plan command received a saved plan file as input. This command\n" +
|
||||
"will output the saved plan. This will not modify the already-existing\n" +
|
||||
"plan. If you wish to generate a new plan, please pass in a configuration\n" +
|
||||
"directory as an argument.\n\n"))
|
||||
}
|
||||
|
||||
// Setup our count hook that keeps track of resource changes
|
||||
countHook := new(CountHook)
|
||||
if b.ContextOpts == nil {
|
||||
b.ContextOpts = new(terraform.ContextOpts)
|
||||
}
|
||||
old := b.ContextOpts.Hooks
|
||||
defer func() { b.ContextOpts.Hooks = old }()
|
||||
b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, countHook)
|
||||
|
||||
// Get our context
|
||||
tfCtx, _, err := b.context(op)
|
||||
if err != nil {
|
||||
runningOp.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
// Setup the state
|
||||
runningOp.State = tfCtx.State()
|
||||
|
||||
// If we're refreshing before plan, perform that
|
||||
if op.PlanRefresh {
|
||||
log.Printf("[INFO] backend/local: plan calling Refresh")
|
||||
|
||||
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planRefreshing) + "\n"))
|
||||
_, err := tfCtx.Refresh()
|
||||
if err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the plan
|
||||
log.Printf("[INFO] backend/local: plan calling Plan")
|
||||
plan, err := tfCtx.Plan()
|
||||
if err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Record state
|
||||
runningOp.PlanEmpty = plan.Diff.Empty()
|
||||
|
||||
// Save the plan to disk
|
||||
if path := op.PlanOutPath; path != "" {
|
||||
// Write the backend if we have one
|
||||
plan.Backend = op.PlanOutBackend
|
||||
|
||||
log.Printf("[INFO] backend/local: writing plan output to: %s", path)
|
||||
f, err := os.Create(path)
|
||||
if err == nil {
|
||||
err = terraform.WritePlan(plan, f)
|
||||
}
|
||||
f.Close()
|
||||
if err != nil {
|
||||
runningOp.Err = fmt.Errorf("Error writing plan file: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Perform some output tasks if we have a CLI to output to.
|
||||
if b.CLI != nil {
|
||||
if plan.Diff.Empty() {
|
||||
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planNoChanges)))
|
||||
return
|
||||
}
|
||||
|
||||
if path := op.PlanOutPath; path == "" {
|
||||
b.CLI.Output(strings.TrimSpace(planHeaderNoOutput) + "\n")
|
||||
} else {
|
||||
b.CLI.Output(fmt.Sprintf(
|
||||
strings.TrimSpace(planHeaderYesOutput)+"\n",
|
||||
path))
|
||||
}
|
||||
|
||||
b.CLI.Output(format.Plan(&format.PlanOpts{
|
||||
Plan: plan,
|
||||
Color: b.Colorize(),
|
||||
ModuleDepth: -1,
|
||||
}))
|
||||
|
||||
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(
|
||||
"[reset][bold]Plan:[reset] "+
|
||||
"%d to add, %d to change, %d to destroy.",
|
||||
countHook.ToAdd+countHook.ToRemoveAndAdd,
|
||||
countHook.ToChange,
|
||||
countHook.ToRemove+countHook.ToRemoveAndAdd)))
|
||||
}
|
||||
}
|
||||
|
||||
const planHeaderNoOutput = `
|
||||
The Terraform execution plan has been generated and is shown below.
|
||||
Resources are shown in alphabetical order for quick scanning. Green resources
|
||||
will be created (or destroyed and then created if an existing resource
|
||||
exists), yellow resources are being changed in-place, and red resources
|
||||
will be destroyed. Cyan entries are data sources to be read.
|
||||
|
||||
Note: You didn't specify an "-out" parameter to save this plan, so when
|
||||
"apply" is called, Terraform can't guarantee this is what will execute.
|
||||
`
|
||||
|
||||
const planHeaderYesOutput = `
|
||||
The Terraform execution plan has been generated and is shown below.
|
||||
Resources are shown in alphabetical order for quick scanning. Green resources
|
||||
will be created (or destroyed and then created if an existing resource
|
||||
exists), yellow resources are being changed in-place, and red resources
|
||||
will be destroyed. Cyan entries are data sources to be read.
|
||||
|
||||
Your plan was also saved to the path below. Call the "apply" subcommand
|
||||
with this plan file and Terraform will exactly execute this execution
|
||||
plan.
|
||||
|
||||
Path: %s
|
||||
`
|
||||
|
||||
const planNoChanges = `
|
||||
[reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
|
||||
|
||||
This means that Terraform could not detect any differences between your
|
||||
configuration and real physical resources that exist. As a result, Terraform
|
||||
doesn't need to do anything.
|
||||
`
|
||||
|
||||
const planRefreshing = `
|
||||
[reset][bold]Refreshing Terraform state in-memory prior to plan...[reset]
|
||||
The refreshed state will be used to calculate this plan, but will not be
|
||||
persisted to local or remote state storage.
|
||||
`
|
|
@ -0,0 +1,183 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestLocal_planBasic(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||
defer modCleanup()
|
||||
|
||||
op := testOperationPlan()
|
||||
op.Module = mod
|
||||
op.PlanRefresh = true
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !p.DiffCalled {
|
||||
t.Fatal("diff should be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocal_planRefreshFalse(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testPlanState())
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||
defer modCleanup()
|
||||
|
||||
op := testOperationPlan()
|
||||
op.Module = mod
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.RefreshCalled {
|
||||
t.Fatal("refresh should not be called")
|
||||
}
|
||||
|
||||
if !run.PlanEmpty {
|
||||
t.Fatal("plan should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocal_planDestroy(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testPlanState())
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||
defer modCleanup()
|
||||
|
||||
outDir := testTempDir(t)
|
||||
defer os.RemoveAll(outDir)
|
||||
planPath := filepath.Join(outDir, "plan.tfplan")
|
||||
|
||||
op := testOperationPlan()
|
||||
op.Destroy = true
|
||||
op.PlanRefresh = true
|
||||
op.Module = mod
|
||||
op.PlanOutPath = planPath
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !p.RefreshCalled {
|
||||
t.Fatal("refresh should be called")
|
||||
}
|
||||
|
||||
if run.PlanEmpty {
|
||||
t.Fatal("plan should not be empty")
|
||||
}
|
||||
|
||||
plan := testReadPlan(t, planPath)
|
||||
for _, m := range plan.Diff.Modules {
|
||||
for _, r := range m.Resources {
|
||||
if !r.Destroy {
|
||||
t.Fatalf("bad: %#v", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocal_planOutPathNoChange(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testPlanState())
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||
defer modCleanup()
|
||||
|
||||
outDir := testTempDir(t)
|
||||
defer os.RemoveAll(outDir)
|
||||
planPath := filepath.Join(outDir, "plan.tfplan")
|
||||
|
||||
op := testOperationPlan()
|
||||
op.Module = mod
|
||||
op.PlanOutPath = planPath
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
if run.Err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
plan := testReadPlan(t, planPath)
|
||||
if !plan.Diff.Empty() {
|
||||
t.Fatalf("expected empty plan to be written")
|
||||
}
|
||||
}
|
||||
|
||||
func testOperationPlan() *backend.Operation {
|
||||
return &backend.Operation{
|
||||
Type: backend.OperationTypePlan,
|
||||
}
|
||||
}
|
||||
|
||||
// testPlanState is just a common state that we use for testing refresh.
|
||||
func testPlanState() *terraform.State {
|
||||
return &terraform.State{
|
||||
Version: 2,
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testReadPlan(t *testing.T, path string) *terraform.Plan {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
p, err := terraform.ReadPlan(f)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
)
|
||||
|
||||
func (b *Local) opRefresh(
|
||||
ctx context.Context,
|
||||
op *backend.Operation,
|
||||
runningOp *backend.RunningOperation) {
|
||||
// Check if our state exists if we're performing a refresh operation. We
|
||||
// only do this if we're managing state with this backend.
|
||||
if b.Backend == nil {
|
||||
if _, err := os.Stat(b.StatePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
runningOp.Err = fmt.Errorf(
|
||||
"The Terraform state file for your infrastructure does not\n"+
|
||||
"exist. The 'refresh' command only works and only makes sense\n"+
|
||||
"when there is existing state that Terraform is managing. Please\n"+
|
||||
"double-check the value given below and try again. If you\n"+
|
||||
"haven't created infrastructure with Terraform yet, use the\n"+
|
||||
"'terraform apply' command.\n\n"+
|
||||
"Path: %s",
|
||||
b.StatePath)
|
||||
return
|
||||
}
|
||||
|
||||
runningOp.Err = fmt.Errorf(
|
||||
"There was an error reading the Terraform state that is needed\n"+
|
||||
"for refreshing. The path and error are shown below.\n\n"+
|
||||
"Path: %s\n\nError: %s",
|
||||
b.StatePath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get our context
|
||||
tfCtx, state, err := b.context(op)
|
||||
if err != nil {
|
||||
runningOp.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
// Set our state
|
||||
runningOp.State = state.State()
|
||||
|
||||
// Perform operation and write the resulting state to the running op
|
||||
newState, err := tfCtx.Refresh()
|
||||
runningOp.State = newState
|
||||
if err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Write and persist the state
|
||||
if err := state.WriteState(newState); err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err)
|
||||
return
|
||||
}
|
||||
if err := state.PersistState(); err != nil {
|
||||
runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestLocal_refresh(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testRefreshState())
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
|
||||
defer modCleanup()
|
||||
|
||||
op := testOperationRefresh()
|
||||
op.Module = mod
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
|
||||
if !p.RefreshCalled {
|
||||
t.Fatal("refresh should be called")
|
||||
}
|
||||
|
||||
checkState(t, b.StateOutPath, `
|
||||
test_instance.foo:
|
||||
ID = yes
|
||||
`)
|
||||
}
|
||||
|
||||
func TestLocal_refreshInput(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testRefreshState())
|
||||
|
||||
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
|
||||
if v, ok := c.Get("value"); !ok || v != "bar" {
|
||||
return fmt.Errorf("no value set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh-var-unset")
|
||||
defer modCleanup()
|
||||
|
||||
// Enable input asking since it is normally disabled by default
|
||||
b.OpInput = true
|
||||
b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"}
|
||||
|
||||
op := testOperationRefresh()
|
||||
op.Module = mod
|
||||
op.UIIn = b.ContextOpts.UIInput
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
|
||||
if !p.RefreshCalled {
|
||||
t.Fatal("refresh should be called")
|
||||
}
|
||||
|
||||
checkState(t, b.StateOutPath, `
|
||||
test_instance.foo:
|
||||
ID = yes
|
||||
`)
|
||||
}
|
||||
|
||||
func TestLocal_refreshValidate(t *testing.T) {
|
||||
b := TestLocal(t)
|
||||
p := TestLocalProvider(t, b, "test")
|
||||
terraform.TestStateFile(t, b.StatePath, testRefreshState())
|
||||
|
||||
p.RefreshFn = nil
|
||||
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
|
||||
|
||||
mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh")
|
||||
defer modCleanup()
|
||||
|
||||
// Enable validation
|
||||
b.OpValidation = true
|
||||
|
||||
op := testOperationRefresh()
|
||||
op.Module = mod
|
||||
|
||||
run, err := b.Operation(context.Background(), op)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
<-run.Done()
|
||||
|
||||
if !p.ValidateCalled {
|
||||
t.Fatal("validate should be called")
|
||||
}
|
||||
|
||||
checkState(t, b.StateOutPath, `
|
||||
test_instance.foo:
|
||||
ID = yes
|
||||
`)
|
||||
}
|
||||
|
||||
func testOperationRefresh() *backend.Operation {
|
||||
return &backend.Operation{
|
||||
Type: backend.OperationTypeRefresh,
|
||||
}
|
||||
}
|
||||
|
||||
// testRefreshState is just a common state that we use for testing refresh.
|
||||
func testRefreshState() *terraform.State {
|
||||
return &terraform.State{
|
||||
Version: 2,
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
Outputs: map[string]*terraform.OutputState{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestLocal_impl(t *testing.T) {
|
||||
var _ backend.Enhanced = new(Local)
|
||||
var _ backend.Local = new(Local)
|
||||
}
|
||||
|
||||
func checkState(t *testing.T, path, expected string) {
|
||||
// Read the state
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
state, err := terraform.ReadState(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(state.String())
|
||||
expected = strings.TrimSpace(expected)
|
||||
if actual != expected {
|
||||
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Code generated by "stringer -type=countHookAction hook_count_action.go"; DO NOT EDIT
|
||||
|
||||
package local
|
||||
|
||||
import "fmt"
|
||||
|
||||
const _countHookAction_name = "countHookActionAddcountHookActionChangecountHookActionRemove"
|
||||
|
||||
var _countHookAction_index = [...]uint8{0, 18, 39, 60}
|
||||
|
||||
func (i countHookAction) String() string {
|
||||
if i >= countHookAction(len(_countHookAction_index)-1) {
|
||||
return fmt.Sprintf("countHookAction(%d)", i)
|
||||
}
|
||||
return _countHookAction_name[_countHookAction_index[i]:_countHookAction_index[i+1]]
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// CountHook is a hook that counts the number of resources
|
||||
// added, removed, changed during the course of an apply.
|
||||
type CountHook struct {
|
||||
Added int
|
||||
Changed int
|
||||
Removed int
|
||||
|
||||
ToAdd int
|
||||
ToChange int
|
||||
ToRemove int
|
||||
ToRemoveAndAdd int
|
||||
|
||||
pending map[string]countHookAction
|
||||
|
||||
sync.Mutex
|
||||
terraform.NilHook
|
||||
}
|
||||
|
||||
func (h *CountHook) Reset() {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
h.pending = nil
|
||||
h.Added = 0
|
||||
h.Changed = 0
|
||||
h.Removed = 0
|
||||
}
|
||||
|
||||
func (h *CountHook) PreApply(
|
||||
n *terraform.InstanceInfo,
|
||||
s *terraform.InstanceState,
|
||||
d *terraform.InstanceDiff) (terraform.HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.pending == nil {
|
||||
h.pending = make(map[string]countHookAction)
|
||||
}
|
||||
|
||||
action := countHookActionChange
|
||||
if d.GetDestroy() {
|
||||
action = countHookActionRemove
|
||||
} else if s.ID == "" {
|
||||
action = countHookActionAdd
|
||||
}
|
||||
|
||||
h.pending[n.HumanId()] = action
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *CountHook) PostApply(
|
||||
n *terraform.InstanceInfo,
|
||||
s *terraform.InstanceState,
|
||||
e error) (terraform.HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.pending != nil {
|
||||
if a, ok := h.pending[n.HumanId()]; ok {
|
||||
delete(h.pending, n.HumanId())
|
||||
|
||||
if e == nil {
|
||||
switch a {
|
||||
case countHookActionAdd:
|
||||
h.Added += 1
|
||||
case countHookActionChange:
|
||||
h.Changed += 1
|
||||
case countHookActionRemove:
|
||||
h.Removed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
func (h *CountHook) PostDiff(
|
||||
n *terraform.InstanceInfo, d *terraform.InstanceDiff) (
|
||||
terraform.HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
// We don't count anything for data sources
|
||||
if strings.HasPrefix(n.Id, "data.") {
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
||||
|
||||
switch d.ChangeType() {
|
||||
case terraform.DiffDestroyCreate:
|
||||
h.ToRemoveAndAdd += 1
|
||||
case terraform.DiffCreate:
|
||||
h.ToAdd += 1
|
||||
case terraform.DiffDestroy:
|
||||
h.ToRemove += 1
|
||||
case terraform.DiffUpdate:
|
||||
h.ToChange += 1
|
||||
}
|
||||
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package local
|
||||
|
||||
//go:generate stringer -type=countHookAction hook_count_action.go
|
||||
|
||||
type countHookAction byte
|
||||
|
||||
const (
|
||||
countHookActionAdd countHookAction = iota
|
||||
countHookActionChange
|
||||
countHookActionRemove
|
||||
)
|
|
@ -0,0 +1,243 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestCountHook_impl(t *testing.T) {
|
||||
var _ terraform.Hook = new(CountHook)
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_DestroyDeposed(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"lorem": &terraform.InstanceDiff{DestroyDeposed: true},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{} // TODO
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 1
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_DestroyOnly(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"foo": &terraform.InstanceDiff{Destroy: true},
|
||||
"bar": &terraform.InstanceDiff{Destroy: true},
|
||||
"lorem": &terraform.InstanceDiff{Destroy: true},
|
||||
"ipsum": &terraform.InstanceDiff{Destroy: true},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{} // TODO
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 4
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_AddOnly(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"foo": &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
|
||||
},
|
||||
},
|
||||
"bar": &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
|
||||
},
|
||||
},
|
||||
"lorem": &terraform.InstanceDiff{
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{RequiresNew: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{}
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 3
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 0
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_ChangeOnly(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"foo": &terraform.InstanceDiff{
|
||||
Destroy: false,
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{},
|
||||
},
|
||||
},
|
||||
"bar": &terraform.InstanceDiff{
|
||||
Destroy: false,
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{},
|
||||
},
|
||||
},
|
||||
"lorem": &terraform.InstanceDiff{
|
||||
Destroy: false,
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{}
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 3
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 0
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_Mixed(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"foo": &terraform.InstanceDiff{
|
||||
Destroy: true,
|
||||
},
|
||||
"bar": &terraform.InstanceDiff{},
|
||||
"lorem": &terraform.InstanceDiff{
|
||||
Destroy: false,
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{},
|
||||
},
|
||||
},
|
||||
"ipsum": &terraform.InstanceDiff{Destroy: true},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{}
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 1
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 2
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_NoChange(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"foo": &terraform.InstanceDiff{},
|
||||
"bar": &terraform.InstanceDiff{},
|
||||
"lorem": &terraform.InstanceDiff{},
|
||||
"ipsum": &terraform.InstanceDiff{},
|
||||
}
|
||||
|
||||
n := &terraform.InstanceInfo{}
|
||||
|
||||
for _, d := range resources {
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 0
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHookPostDiff_DataSource(t *testing.T) {
|
||||
h := new(CountHook)
|
||||
|
||||
resources := map[string]*terraform.InstanceDiff{
|
||||
"data.foo": &terraform.InstanceDiff{
|
||||
Destroy: true,
|
||||
},
|
||||
"data.bar": &terraform.InstanceDiff{},
|
||||
"data.lorem": &terraform.InstanceDiff{
|
||||
Destroy: false,
|
||||
Attributes: map[string]*terraform.ResourceAttrDiff{
|
||||
"foo": &terraform.ResourceAttrDiff{},
|
||||
},
|
||||
},
|
||||
"data.ipsum": &terraform.InstanceDiff{Destroy: true},
|
||||
}
|
||||
|
||||
for k, d := range resources {
|
||||
n := &terraform.InstanceInfo{Id: k}
|
||||
h.PostDiff(n, d)
|
||||
}
|
||||
|
||||
expected := new(CountHook)
|
||||
expected.ToAdd = 0
|
||||
expected.ToChange = 0
|
||||
expected.ToRemoveAndAdd = 0
|
||||
expected.ToRemove = 0
|
||||
|
||||
if !reflect.DeepEqual(expected, h) {
|
||||
t.Fatalf("Expected %#v, got %#v instead.",
|
||||
expected, h)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// StateHook is a hook that continuously updates the state by calling
|
||||
// WriteState on a state.State.
|
||||
type StateHook struct {
|
||||
terraform.NilHook
|
||||
sync.Mutex
|
||||
|
||||
State state.State
|
||||
}
|
||||
|
||||
func (h *StateHook) PostStateUpdate(
|
||||
s *terraform.State) (terraform.HookAction, error) {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if h.State != nil {
|
||||
// Write the new state
|
||||
if err := h.State.WriteState(s); err != nil {
|
||||
return terraform.HookActionHalt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Continue forth
|
||||
return terraform.HookActionContinue, nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestStateHook_impl(t *testing.T) {
|
||||
var _ terraform.Hook = new(StateHook)
|
||||
}
|
||||
|
||||
func TestStateHook(t *testing.T) {
|
||||
is := &state.InmemState{}
|
||||
var hook terraform.Hook = &StateHook{State: is}
|
||||
|
||||
s := state.TestStateInitial()
|
||||
action, err := hook.PostStateUpdate(s)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if action != terraform.HookActionContinue {
|
||||
t.Fatalf("bad: %v", action)
|
||||
}
|
||||
if !is.State().Equal(s) {
|
||||
t.Fatalf("bad state: %#v", is.State())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/logging"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
if testing.Verbose() {
|
||||
// if we're verbose, use the logging requested by TF_LOG
|
||||
logging.SetOutput()
|
||||
} else {
|
||||
// otherwise silence all logs
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
||||
|
||||
resource "test_instance" "bar" {
|
||||
error = "true"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
|
||||
# This is here because at some point it caused a test failure
|
||||
network_interface {
|
||||
device_index = 0
|
||||
description = "Main network interface"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
variable "should_ask" {}
|
||||
|
||||
provider "test" {
|
||||
value = "${var.should_ask}"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
foo = "bar"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_instance" "foo" {
|
||||
ami = "bar"
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// TestLocal returns a configured Local struct with temporary paths and
|
||||
// in-memory ContextOpts.
|
||||
//
|
||||
// No operations will be called on the returned value, so you can still set
|
||||
// public fields without any locks.
|
||||
func TestLocal(t *testing.T) *Local {
|
||||
tempDir := testTempDir(t)
|
||||
return &Local{
|
||||
StatePath: filepath.Join(tempDir, "state.tfstate"),
|
||||
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
|
||||
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
|
||||
ContextOpts: &terraform.ContextOpts{},
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalProvider modifies the ContextOpts of the *Local parameter to
|
||||
// have a provider with the given name.
|
||||
func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResourceProvider {
|
||||
// Build a mock resource provider for in-memory operations
|
||||
p := new(terraform.MockResourceProvider)
|
||||
p.DiffReturn = &terraform.InstanceDiff{}
|
||||
p.RefreshFn = func(
|
||||
info *terraform.InstanceInfo,
|
||||
s *terraform.InstanceState) (*terraform.InstanceState, error) {
|
||||
return s, nil
|
||||
}
|
||||
p.ResourcesReturn = []terraform.ResourceType{
|
||||
terraform.ResourceType{
|
||||
Name: "test_instance",
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize the opts
|
||||
if b.ContextOpts == nil {
|
||||
b.ContextOpts = &terraform.ContextOpts{}
|
||||
}
|
||||
if b.ContextOpts.Providers == nil {
|
||||
b.ContextOpts.Providers = make(map[string]terraform.ResourceProviderFactory)
|
||||
}
|
||||
|
||||
// Setup our provider
|
||||
b.ContextOpts.Providers[name] = func() (terraform.ResourceProvider, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func testTempDir(t *testing.T) string {
|
||||
d, err := ioutil.TempDir("", "tf")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
Loading…
Reference in New Issue