Merge pull request #29755 from hashicorp/jbardin/first-plan-lineage

Check for stale plan with no state metadata
This commit is contained in:
James Bardin 2021-10-14 13:31:30 -04:00 committed by GitHub
commit ef3c98466d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 45 additions and 13 deletions

View File

@ -290,8 +290,22 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
// has changed since the plan was created. (All of the "real-world" // has changed since the plan was created. (All of the "real-world"
// state manager implementations support this, but simpler test backends // state manager implementations support this, but simpler test backends
// may not.) // may not.)
if currentStateMeta.Lineage != "" && priorStateFile.Lineage != "" {
if priorStateFile.Serial != currentStateMeta.Serial || priorStateFile.Lineage != currentStateMeta.Lineage { // Because the plan always contains a state, even if it is empty, the
// first plan to be applied will have empty snapshot metadata. In this
// case we compare only the serial in order to provide a more correct
// error.
firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0
switch {
case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage:
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Saved plan does not match the given state",
"The given plan file can not be applied because it was created from a different state lineage.",
))
case priorStateFile.Serial != currentStateMeta.Serial:
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"Saved plan is stale", "Saved plan is stale",
@ -299,7 +313,6 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
)) ))
} }
} }
}
// When we're applying a saved plan, the input state is the "prior state" // When we're applying a saved plan, the input state is the "prior state"
// recorded in the plan, which incorporates the result of all of the // recorded in the plan, which incorporates the result of all of the
// refreshing we did while building the plan. // refreshing we did while building the plan.

View File

@ -710,7 +710,6 @@ func TestApply_plan(t *testing.T) {
} }
func TestApply_plan_backup(t *testing.T) { func TestApply_plan_backup(t *testing.T) {
planPath := applyFixturePlanFile(t)
statePath := testTempFile(t) statePath := testTempFile(t)
backupPath := testTempFile(t) backupPath := testTempFile(t)
@ -724,11 +723,17 @@ func TestApply_plan_backup(t *testing.T) {
} }
// create a state file that needs to be backed up // create a state file that needs to be backed up
err := statemgr.NewFilesystem(statePath).WriteState(states.NewState()) fs := statemgr.NewFilesystem(statePath)
fs.StateSnapshotMeta()
err := fs.WriteState(states.NewState())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// the plan file must contain the metadata from the prior state to be
// backed up
planPath := applyFixturePlanFileMatchState(t, fs.StateSnapshotMeta())
args := []string{ args := []string{
"-state", statePath, "-state", statePath,
"-backup", backupPath, "-backup", backupPath,
@ -2280,6 +2285,13 @@ func applyFixtureProvider() *terraform.MockProvider {
// a single change to create the test_instance.foo that is included in the // a single change to create the test_instance.foo that is included in the
// "apply" test fixture, returning the location of that plan file. // "apply" test fixture, returning the location of that plan file.
func applyFixturePlanFile(t *testing.T) string { func applyFixturePlanFile(t *testing.T) string {
return applyFixturePlanFileMatchState(t, statemgr.SnapshotMeta{})
}
// applyFixturePlanFileMatchState creates a planfile like applyFixturePlanFile,
// but inserts the state meta information if that plan must match a preexisting
// state.
func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMeta) string {
_, snap := testModuleWithSnapshot(t, "apply") _, snap := testModuleWithSnapshot(t, "apply")
plannedVal := cty.ObjectVal(map[string]cty.Value{ plannedVal := cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String), "id": cty.UnknownVal(cty.String),
@ -2310,11 +2322,12 @@ func applyFixturePlanFile(t *testing.T) string {
After: plannedValRaw, After: plannedValRaw,
}, },
}) })
return testPlanFile( return testPlanFileMatchState(
t, t,
snap, snap,
states.NewState(), states.NewState(),
plan, plan,
stateMeta,
) )
} }

View File

@ -229,15 +229,21 @@ func testPlan(t *testing.T) *plans.Plan {
} }
func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) string { func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) string {
return testPlanFileMatchState(t, configSnap, state, plan, statemgr.SnapshotMeta{})
}
func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan, stateMeta statemgr.SnapshotMeta) string {
t.Helper() t.Helper()
stateFile := &statefile.File{ stateFile := &statefile.File{
Lineage: "", Lineage: stateMeta.Lineage,
Serial: stateMeta.Serial,
State: state, State: state,
TerraformVersion: version.SemVer, TerraformVersion: version.SemVer,
} }
prevStateFile := &statefile.File{ prevStateFile := &statefile.File{
Lineage: "", Lineage: stateMeta.Lineage,
Serial: stateMeta.Serial,
State: state, // we just assume no changes detected during refresh State: state, // we just assume no changes detected during refresh
TerraformVersion: version.SemVer, TerraformVersion: version.SemVer,
} }