Skip to content

Commit

Permalink
command: Refuse to apply plan if its state doesn't match current
Browse files Browse the repository at this point in the history
After running "terraform plan -out=tfplan" and then
"terraform apply tfplan" the plan file is left on disk and could
previously potentially be applied a second time.

Here we add a new constraint that prevents the use of a plan that was
not produced from the current state, thus avoiding that problem.

It will also reduce race conditions (on the human timescale) between
running "plan" and later running "apply", in environments where multiple
people/processes are using Terraform with the same remote state. This
hazard cannot be eliminated entirely without proper locking, but the
with this change in place the race condition is only for two concurrent
*applies*, as opposed to overlapping of the whole time period between plan
and apply.
  • Loading branch information
apparentlymart committed May 8, 2016
1 parent f92fce3 commit f66f674
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 0 deletions.
47 changes: 47 additions & 0 deletions command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return nil, false, fmt.Errorf("Error loading plan: %s", err)
}

// The plan's state must mach our current state, to avoid
// accidentally re-applying a state or applying a plan
// that was generated against a different lineage of states.
if state != nil {
if planState := state.State(); planState != nil {
stateMatch, err := m.stateMatchesCurrent(planState)

if err != nil {
return nil, false, fmt.Errorf("Error verifying plan state: %s", err)
}
if !stateMatch {
return nil, false, fmt.Errorf(
"Plan does not apply to current state.\n\n" +
"Perhaps you have tried to apply a plan that was already applied,\n" +
"or somebody else has applied another plan first.\n",
)
}
}
}

// Set our state
m.state = state
m.stateOutPath = statePath
Expand Down Expand Up @@ -162,6 +182,33 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
return ctx, false, nil
}

func (m *Meta) stateMatchesCurrent(state *terraform.State) (bool, error) {
currentStateResult, err := m.State()
if err != nil {
return false, err
}
currentState := currentStateResult.State()

// If we have no current state then no state can match.
if currentState == nil {
return false, nil
}

if !currentState.SameLineage(state) {
return false, nil
}

if currentState.Serial != state.Serial {
return false, nil
}

if !currentState.Equal(state) {
return false, nil
}

return true, nil
}

// DataDir returns the directory where local data will be stored.
func (m *Meta) DataDir() string {
dataDir := DefaultDataDirectory
Expand Down
3 changes: 3 additions & 0 deletions terraform/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ func (s *State) RootModule() *ModuleState {
}

// Equal tests if one state is equal to another.
//
// "Equal" here means "contains the same resources", so two states with
// different serials/lineages but the same contents will return true.
func (s *State) Equal(other *State) bool {
// If one is nil, we do a direct check
if s == nil || other == nil {
Expand Down

0 comments on commit f66f674

Please sign in to comment.