diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 4124b2abd..0e1daef40 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -275,6 +275,13 @@ type Operation struct { // the variables set in the plan are used instead, and they must be valid. AllowUnsetVariables bool + // When loading a plan file for a read-only operation, we may want to + // disable the state lineage checks which are only relevant for operations + // which can modify state. An example where this is important is showing + // a plan which was prepared against a non-default state file, because the + // lineage checks are always against the default state. + DisablePlanFileStateLineageChecks bool + // View implements the logic for all UI interactions. View views.Operation diff --git a/internal/backend/local/backend_local.go b/internal/backend/local/backend_local.go index 6082bfdf6..a4a4fb67e 100644 --- a/internal/backend/local/backend_local.go +++ b/internal/backend/local/backend_local.go @@ -284,7 +284,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, )) return nil, snap, diags } - if currentStateMeta != nil { + if !op.DisablePlanFileStateLineageChecks && currentStateMeta != nil { // If the caller sets this, we require that the stored prior state // has the same metadata, which is an extra safety check that nothing // has changed since the plan was created. (All of the "real-world" diff --git a/internal/command/show.go b/internal/command/show.go index 6ae66beeb..728ea9872 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -94,6 +94,7 @@ func (c *ShowCommand) Run(args []string) int { opReq.PlanFile = planFile opReq.ConfigLoader, err = c.initConfigLoader() opReq.AllowUnsetVariables = true + opReq.DisablePlanFileStateLineageChecks = true if err != nil { diags = diags.Append(err) c.showDiagnostics(diags) diff --git a/internal/command/show_test.go b/internal/command/show_test.go index ea266d2cb..25504d2c2 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -15,7 +15,9 @@ import ( "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/version" "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" ) @@ -575,6 +577,52 @@ func TestShow_json_output_state(t *testing.T) { } } +func TestShow_planWithNonDefaultStateLineage(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("show"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Write default state file with a testing lineage ("fake-for-testing") + testStateFileDefault(t, testState()) + + // Create a plan with a different lineage, which we should still be able + // to show + _, snap := testModuleWithSnapshot(t, "show") + state := testState() + plan := testPlan(t) + stateMeta := statemgr.SnapshotMeta{ + Lineage: "fake-for-plan", + Serial: 1, + TerraformVersion: version.SemVer, + } + planPath := testPlanFileMatchState(t, snap, state, plan, stateMeta) + + ui := cli.NewMockUi() + view, done := testView(t) + c := &ShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{ + planPath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + want := `No changes. Your infrastructure matches the configuration.` + got := done(t).Stdout() + if !strings.Contains(got, want) { + t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got) + } +} + // showFixtureSchema returns a schema suitable for processing the configuration // in testdata/show. This schema should be assigned to a mock provider // named "test".