terraform/internal/command/show.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

410 lines
13 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
2014-07-12 22:47:31 -04:00
package command
import (
"context"
"errors"
2014-07-12 22:47:31 -04:00
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/cloud/cloudplan"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/plans/planfile"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
2014-07-12 22:47:31 -04:00
)
// Many of the methods we get data from can emit special error types if they're
// pretty sure about the file type but still can't use it. But they can't all do
// that! So, we have to do a couple ourselves if we want to preserve that data.
type errUnusableDataMisc struct {
inner error
kind string
}
func errUnusable(err error, kind string) *errUnusableDataMisc {
return &errUnusableDataMisc{inner: err, kind: kind}
}
func (e *errUnusableDataMisc) Error() string {
return e.inner.Error()
}
func (e *errUnusableDataMisc) Unwrap() error {
return e.inner
}
2014-07-12 22:47:31 -04:00
// ShowCommand is a Command implementation that reads and outputs the
// contents of a Terraform plan or state file.
type ShowCommand struct {
Meta
viewType arguments.ViewType
2014-07-12 22:47:31 -04:00
}
func (c *ShowCommand) Run(rawArgs []string) int {
// Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common)
2014-07-12 22:47:31 -04:00
// Parse and validate flags
args, diags := arguments.ParseShow(rawArgs)
if diags.HasErrors() {
c.View.Diagnostics(diags)
c.View.HelpPrompt("show")
2014-07-12 22:47:31 -04:00
return 1
}
c.viewType = args.ViewType
// Set up view
view := views.NewShow(args.ViewType, c.View)
loader, err := c.initConfigLoader()
if err != nil {
diags = diags.Append(err)
view.Diagnostics(diags)
return 1
}
var varDiags tfdiags.Diagnostics
c.VariableValues, varDiags = args.Vars.CollectValues(func(filename string, src []byte) {
loader.Parser().ForceFileSource(filename, src)
})
diags = diags.Append(varDiags)
// Check for user-supplied plugin path
if c.pluginPath, err = c.loadPluginPath(); err != nil {
diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
view.Diagnostics(diags)
return 1
}
// Get the data we need to display
plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path)
diags = diags.Append(showDiags)
if showDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Display the data
return view.Display(config, plan, jsonPlan, stateFile, schemas)
}
func (c *ShowCommand) Help() string {
helpText := `
Usage: terraform [global options] show [options] [path]
Reads and outputs a Terraform state or plan file in a human-readable
form. If no path is specified, the current state will be shown.
Options:
-no-color If specified, output won't contain any color.
-json If specified, output the Terraform plan or state in
a machine-readable form.
`
return strings.TrimSpace(helpText)
}
func (c *ShowCommand) Synopsis() string {
return "Show the current state or a saved plan"
}
func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *terraform.Schemas, tfdiags.Diagnostics) {
var diags, showDiags tfdiags.Diagnostics
var plan *plans.Plan
var jsonPlan *cloudplan.RemotePlanJSON
var stateFile *statefile.File
var config *configs.Config
var schemas *terraform.Schemas
// No plan file or state file argument provided,
// so get the latest state snapshot
if path == "" {
stateFile, showDiags = c.showFromLatestStateSnapshot()
diags = diags.Append(showDiags)
if showDiags.HasErrors() {
return plan, jsonPlan, stateFile, config, schemas, diags
2014-07-12 22:47:31 -04:00
}
}
// Plan file or state file argument provided,
// so try to load the argument as a plan file first.
// If that fails, try to load it as a statefile.
if path != "" {
plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path)
diags = diags.Append(showDiags)
if showDiags.HasErrors() {
return plan, jsonPlan, stateFile, config, schemas, diags
2017-01-18 23:50:45 -05:00
}
2014-07-12 22:47:31 -04:00
}
// Get schemas, if possible
if config != nil || stateFile != nil {
2022-08-30 18:01:44 -04:00
schemas, diags = c.MaybeGetSchemas(stateFile.State, config)
2022-08-25 15:57:40 -04:00
if diags.HasErrors() {
return plan, jsonPlan, stateFile, config, schemas, diags
}
}
return plan, jsonPlan, stateFile, config, schemas, diags
}
func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Load the backend
PSS: Update how commands access backends, so both `backend` and `state_store` configuration can be used (#37569) * Add a generic method for loading an operations backend in non-init commands * Refactor commands to use new prepareBackend method: group 1 * Refactor commands to use new prepareBackend method: group 2, where config parsing needs to be explicitly added * Refactor commands to use new prepareBackend method: group 3, where we can use already parsed config * Additional, more nested, places where logic for accessing backends needs to be refactored * Remove duplicated comment * Add test coverage of `(m *Meta) prepareBackend()` * Add TODO related to using plans for backend/state_store config in apply commands * Add `testStateStoreMockWithChunkNegotiation` test helper * Add assertions to tests about the backend (remote-state, local, etc) in use within operations backend * Stop prepareBackend taking locks as argument * Code comment in prepareBackend * Replace c.Meta.prepareBackend with c.prepareBackend * Change `c.Meta.loadSingleModule` to `c.loadSingleModule` * Rename (Meta).prepareBackend to (Meta).backend, update godoc comment to make relationship to (Meta).Backend more obvious. * Revert change from config.Module to config.Root.Module * Update `(m *Meta) backend` method to parse config itself, and also to adhere to calling code's viewtype instructions * Update all tests and calling code following previous commit * Change how an operations backend is obtained by autocomplete code * Update autocomplete to return nil if no workspace names are returned from the backend * Add test coverage for autocompleting workspace names when using a pluggable state store * Fix output command: pass view type data to new `backend` method * Fix in plan command: pass correct view type to `backend` method * Fix `providers schema` command to use correct viewtype when preparing a backend
2025-11-03 12:57:20 -05:00
b, backendDiags := c.backend(".", c.viewType)
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
return nil, diags
}
c.ignoreRemoteVersionConflict(b)
// Load the workspace
workspace, err := c.Workspace()
if err != nil {
diags = diags.Append(fmt.Errorf("error selecting workspace: %s", err))
return nil, diags
2014-07-12 22:47:31 -04:00
}
// Get the latest state snapshot from the backend for the current workspace
stateFile, stateErr := getStateFromBackend(b, workspace)
if stateErr != nil {
diags = diags.Append(stateErr)
return nil, diags
}
return stateFile, diags
2014-07-12 22:47:31 -04:00
}
func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var planErr, stateErr error
var plan *plans.Plan
var jsonPlan *cloudplan.RemotePlanJSON
var stateFile *statefile.File
var config *configs.Config
2014-07-12 22:47:31 -04:00
// Path might be a local plan file, a bookmark to a saved cloud plan, or a
// state file. First, try to get a plan and associated data from a local
// plan file. If that fails, try to get a json plan from the path argument.
// If that fails, try to get the statefile from the path argument.
plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path)
if planErr != nil {
stateFile, stateErr = getStateFromPath(path)
if stateErr != nil {
// To avoid spamming the user with irrelevant errors, first check to
// see if one of our errors happens to know for a fact what file
// type we were dealing with. If so, then we can ignore the other
// ones (which are likely to be something unhelpful like "not a
// valid zip file"). If not, we can fall back to dumping whatever
// we've got.
var unLocal *planfile.ErrUnusableLocalPlan
var unState *statefile.ErrUnusableState
var unMisc *errUnusableDataMisc
if errors.As(planErr, &unLocal) {
diags = diags.Append(
tfdiags.Sourceless(
tfdiags.Error,
"Couldn't show local plan",
fmt.Sprintf("Plan read error: %s", unLocal),
),
)
} else if errors.As(planErr, &unMisc) {
diags = diags.Append(
tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Couldn't show %s", unMisc.kind),
fmt.Sprintf("Plan read error: %s", unMisc),
),
)
} else if errors.As(stateErr, &unState) {
diags = diags.Append(
tfdiags.Sourceless(
tfdiags.Error,
"Couldn't show state file",
fmt.Sprintf("Plan read error: %s", unState),
),
)
} else if errors.As(stateErr, &unMisc) {
diags = diags.Append(
tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Couldn't show %s", unMisc.kind),
fmt.Sprintf("Plan read error: %s", unMisc),
),
)
} else {
// Ok, give up and show the really big error
diags = diags.Append(
tfdiags.Sourceless(
tfdiags.Error,
"Failed to read the given file as a state or plan file",
fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr),
),
)
}
return nil, nil, nil, nil, diags
}
}
return plan, jsonPlan, stateFile, config, diags
2014-07-12 22:47:31 -04:00
}
// getPlanFromPath returns a plan, json plan, statefile, and config if the
// user-supplied path points to either a local or cloud plan file. Note that
// some of the return values will be nil no matter what; local plan files do not
// yield a json plan, and cloud plans do not yield real plan/state/config
// structs. An error generally suggests that the given path is either a
// directory or a statefile.
func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
var err error
var plan *plans.Plan
var jsonPlan *cloudplan.RemotePlanJSON
var stateFile *statefile.File
var config *configs.Config
pf, err := planfile.OpenWrapped(path)
if err != nil {
return nil, nil, nil, nil, err
}
if lp, ok := pf.Local(); ok {
plan, stateFile, config, err = getDataFromPlanfileReader(lp, c.Meta.AllowExperimentalFeatures, c.Meta.VariableValues)
} else if cp, ok := pf.Cloud(); ok {
redacted := c.viewType != arguments.ViewJSON
jsonPlan, err = c.getDataFromCloudPlan(cp, redacted)
}
return plan, jsonPlan, stateFile, config, err
}
func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool) (*cloudplan.RemotePlanJSON, error) {
// Set up the backend
PSS: Update how commands access backends, so both `backend` and `state_store` configuration can be used (#37569) * Add a generic method for loading an operations backend in non-init commands * Refactor commands to use new prepareBackend method: group 1 * Refactor commands to use new prepareBackend method: group 2, where config parsing needs to be explicitly added * Refactor commands to use new prepareBackend method: group 3, where we can use already parsed config * Additional, more nested, places where logic for accessing backends needs to be refactored * Remove duplicated comment * Add test coverage of `(m *Meta) prepareBackend()` * Add TODO related to using plans for backend/state_store config in apply commands * Add `testStateStoreMockWithChunkNegotiation` test helper * Add assertions to tests about the backend (remote-state, local, etc) in use within operations backend * Stop prepareBackend taking locks as argument * Code comment in prepareBackend * Replace c.Meta.prepareBackend with c.prepareBackend * Change `c.Meta.loadSingleModule` to `c.loadSingleModule` * Rename (Meta).prepareBackend to (Meta).backend, update godoc comment to make relationship to (Meta).Backend more obvious. * Revert change from config.Module to config.Root.Module * Update `(m *Meta) backend` method to parse config itself, and also to adhere to calling code's viewtype instructions * Update all tests and calling code following previous commit * Change how an operations backend is obtained by autocomplete code * Update autocomplete to return nil if no workspace names are returned from the backend * Add test coverage for autocompleting workspace names when using a pluggable state store * Fix output command: pass view type data to new `backend` method * Fix in plan command: pass correct view type to `backend` method * Fix `providers schema` command to use correct viewtype when preparing a backend
2025-11-03 12:57:20 -05:00
b, diags := c.backend(".", c.viewType)
if diags.HasErrors() {
return nil, errUnusable(diags.Err(), "cloud plan")
}
// Cloud plans only work if we're cloud.
cl, ok := b.(*cloud.Cloud)
if !ok {
errMessage := fmt.Sprintf("can't show a saved cloud plan unless the current root module is connected to %s", cl.AppName())
return nil, errUnusable(errors.New(errMessage), "cloud plan")
}
result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted)
if err != nil {
err = errUnusable(err, "cloud plan")
}
return result, err
}
// getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file.
func getDataFromPlanfileReader(planReader *planfile.Reader, allowLanguageExperiments bool, variableValues map[string]arguments.UnparsedVariableValue) (*plans.Plan, *statefile.File, *configs.Config, error) {
// Get plan
plan, err := planReader.ReadPlan()
if err != nil {
return nil, nil, nil, err
}
// Get statefile
stateFile, err := planReader.ReadStateFile()
if err != nil {
return nil, nil, nil, err
}
// Get config
config, diags := readConfig(planReader, allowLanguageExperiments, variableValues)
if diags.HasErrors() {
return nil, nil, nil, errUnusable(diags.Err(), "local plan")
}
return plan, stateFile, config, err
}
func readConfig(r *planfile.Reader, allowLanguageExperiments bool, variableValues map[string]arguments.UnparsedVariableValue) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
snap, err := r.ReadConfigSnapshot()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to read configuration from plan file",
fmt.Sprintf("The configuration file snapshot in the plan file could not be read: %s.", err),
))
return nil, diags
}
loader := configload.NewLoaderFromSnapshot(snap)
loader.AllowLanguageExperiments(allowLanguageExperiments)
rootMod, rootDiags := loader.LoadRootModule(snap.Modules[""].Dir)
diags = diags.Append(rootDiags)
if rootDiags.HasErrors() {
return nil, diags
}
variables, varDiags := backendrun.ParseVariableValues(variableValues, rootMod.Variables)
diags = diags.Append(varDiags)
if diags.HasErrors() {
return nil, diags
}
config, buildDiags := terraform.BuildConfigWithGraph(
rootMod,
loader.ModuleWalker(),
variables,
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
)
diags = diags.Append(buildDiags)
if buildDiags.HasErrors() {
return nil, diags
}
return config, diags
}
// getStateFromPath returns a statefile if the user-supplied path points to a statefile.
func getStateFromPath(path string) (*statefile.File, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Error loading statefile: %w", err)
}
defer file.Close()
var stateFile *statefile.File
stateFile, err = statefile.Read(file)
if err != nil {
return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err)
}
return stateFile, nil
}
// getStateFromBackend returns the State for the current workspace, if available.
func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, error) {
// Get the state store for the given workspace
stateStore, sDiags := b.StateMgr(workspace)
if sDiags.HasErrors() {
return nil, fmt.Errorf("Failed to load state manager: %w", sDiags.Err())
}
// Refresh the state store with the latest state snapshot from persistent storage
if err := stateStore.RefreshState(); err != nil {
return nil, fmt.Errorf("Failed to load state: %w", err)
}
// Get the latest state snapshot and return it
stateFile := statemgr.Export(stateStore)
return stateFile, nil
}