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