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:
Mitchell Hashimoto 2017-01-18 20:47:56 -08:00
parent 8a070ddef0
commit 397e1b3132
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
22 changed files with 1729 additions and 0 deletions

211
backend/local/backend.go Normal file
View File

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

View File

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

View File

@ -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",
},
},
},
},
},
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

111
backend/local/hook_count.go Normal file
View File

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

View File

@ -0,0 +1,11 @@
package local
//go:generate stringer -type=countHookAction hook_count_action.go
type countHookAction byte
const (
countHookActionAdd countHookAction = iota
countHookActionChange
countHookActionRemove
)

View File

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

View File

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

View File

@ -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())
}
}

View File

@ -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())
}

View File

@ -0,0 +1,7 @@
resource "test_instance" "foo" {
ami = "bar"
}
resource "test_instance" "bar" {
error = "true"
}

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
}

View File

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

View File

@ -0,0 +1,9 @@
variable "should_ask" {}
provider "test" {
value = "${var.should_ask}"
}
resource "test_instance" "foo" {
foo = "bar"
}

View File

@ -0,0 +1,3 @@
resource "test_instance" "foo" {
ami = "bar"
}

66
backend/local/testing.go Normal file
View File

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