github.com/opentofu/opentofu@v1.7.1/internal/command/show.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/opentofu/opentofu/internal/backend"
    16  	"github.com/opentofu/opentofu/internal/cloud"
    17  	"github.com/opentofu/opentofu/internal/cloud/cloudplan"
    18  	"github.com/opentofu/opentofu/internal/command/arguments"
    19  	"github.com/opentofu/opentofu/internal/command/views"
    20  	"github.com/opentofu/opentofu/internal/configs"
    21  	"github.com/opentofu/opentofu/internal/encryption"
    22  	"github.com/opentofu/opentofu/internal/plans"
    23  	"github.com/opentofu/opentofu/internal/plans/planfile"
    24  	"github.com/opentofu/opentofu/internal/states/statefile"
    25  	"github.com/opentofu/opentofu/internal/states/statemgr"
    26  	"github.com/opentofu/opentofu/internal/tfdiags"
    27  	"github.com/opentofu/opentofu/internal/tofu"
    28  	"github.com/opentofu/opentofu/internal/tofumigrate"
    29  )
    30  
    31  // Many of the methods we get data from can emit special error types if they're
    32  // pretty sure about the file type but still can't use it. But they can't all do
    33  // that! So, we have to do a couple ourselves if we want to preserve that data.
    34  type errUnusableDataMisc struct {
    35  	inner error
    36  	kind  string
    37  }
    38  
    39  func errUnusable(err error, kind string) *errUnusableDataMisc {
    40  	return &errUnusableDataMisc{inner: err, kind: kind}
    41  }
    42  func (e *errUnusableDataMisc) Error() string {
    43  	return e.inner.Error()
    44  }
    45  func (e *errUnusableDataMisc) Unwrap() error {
    46  	return e.inner
    47  }
    48  
    49  // ShowCommand is a Command implementation that reads and outputs the
    50  // contents of a OpenTofu plan or state file.
    51  type ShowCommand struct {
    52  	Meta
    53  	viewType arguments.ViewType
    54  }
    55  
    56  func (c *ShowCommand) Run(rawArgs []string) int {
    57  	// Parse and apply global view arguments
    58  	common, rawArgs := arguments.ParseView(rawArgs)
    59  	c.View.Configure(common)
    60  
    61  	// Parse and validate flags
    62  	args, diags := arguments.ParseShow(rawArgs)
    63  	if diags.HasErrors() {
    64  		c.View.Diagnostics(diags)
    65  		c.View.HelpPrompt("show")
    66  		return 1
    67  	}
    68  	c.viewType = args.ViewType
    69  
    70  	// Set up view
    71  	view := views.NewShow(args.ViewType, c.View)
    72  
    73  	// Check for user-supplied plugin path
    74  	var err error
    75  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    76  		diags = diags.Append(fmt.Errorf("error loading plugin path: %w", err))
    77  		view.Diagnostics(diags)
    78  		return 1
    79  	}
    80  
    81  	// Load the encryption configuration
    82  	enc, encDiags := c.Encryption()
    83  	diags = diags.Append(encDiags)
    84  	if encDiags.HasErrors() {
    85  		c.showDiagnostics(diags)
    86  		return 1
    87  	}
    88  
    89  	// Get the data we need to display
    90  	plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path, enc)
    91  	diags = diags.Append(showDiags)
    92  	if showDiags.HasErrors() {
    93  		view.Diagnostics(diags)
    94  		return 1
    95  	}
    96  
    97  	// Display the data
    98  	return view.Display(config, plan, jsonPlan, stateFile, schemas)
    99  }
   100  
   101  func (c *ShowCommand) Help() string {
   102  	helpText := `
   103  Usage: tofu [global options] show [options] [path]
   104  
   105    Reads and outputs a OpenTofu state or plan file in a human-readable
   106    form. If no path is specified, the current state will be shown.
   107  
   108  Options:
   109  
   110    -no-color           If specified, output won't contain any color.
   111    -json               If specified, output the OpenTofu plan or state in
   112                        a machine-readable form.
   113  
   114  `
   115  	return strings.TrimSpace(helpText)
   116  }
   117  
   118  func (c *ShowCommand) Synopsis() string {
   119  	return "Show the current state or a saved plan"
   120  }
   121  
   122  func (c *ShowCommand) show(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) {
   123  	var diags, showDiags, migrateDiags tfdiags.Diagnostics
   124  	var plan *plans.Plan
   125  	var jsonPlan *cloudplan.RemotePlanJSON
   126  	var stateFile *statefile.File
   127  	var config *configs.Config
   128  	var schemas *tofu.Schemas
   129  
   130  	// No plan file or state file argument provided,
   131  	// so get the latest state snapshot
   132  	if path == "" {
   133  		stateFile, showDiags = c.showFromLatestStateSnapshot(enc)
   134  		diags = diags.Append(showDiags)
   135  		if showDiags.HasErrors() {
   136  			return plan, jsonPlan, stateFile, config, schemas, diags
   137  		}
   138  	}
   139  
   140  	// Plan file or state file argument provided,
   141  	// so try to load the argument as a plan file first.
   142  	// If that fails, try to load it as a statefile.
   143  	if path != "" {
   144  		plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path, enc)
   145  		diags = diags.Append(showDiags)
   146  		if showDiags.HasErrors() {
   147  			return plan, jsonPlan, stateFile, config, schemas, diags
   148  		}
   149  	}
   150  
   151  	if stateFile != nil {
   152  		stateFile.State, migrateDiags = tofumigrate.MigrateStateProviderAddresses(config, stateFile.State)
   153  		diags = diags.Append(migrateDiags)
   154  		if migrateDiags.HasErrors() {
   155  			return plan, jsonPlan, stateFile, config, schemas, diags
   156  		}
   157  	}
   158  
   159  	// Get schemas, if possible
   160  	if config != nil || stateFile != nil {
   161  		schemas, diags = c.MaybeGetSchemas(stateFile.State, config)
   162  		if diags.HasErrors() {
   163  			return plan, jsonPlan, stateFile, config, schemas, diags
   164  		}
   165  	}
   166  
   167  	return plan, jsonPlan, stateFile, config, schemas, diags
   168  }
   169  func (c *ShowCommand) showFromLatestStateSnapshot(enc encryption.Encryption) (*statefile.File, tfdiags.Diagnostics) {
   170  	var diags tfdiags.Diagnostics
   171  
   172  	// Load the backend
   173  	b, backendDiags := c.Backend(nil, enc.State())
   174  	diags = diags.Append(backendDiags)
   175  	if backendDiags.HasErrors() {
   176  		return nil, diags
   177  	}
   178  	c.ignoreRemoteVersionConflict(b)
   179  
   180  	// Load the workspace
   181  	workspace, err := c.Workspace()
   182  	if err != nil {
   183  		diags = diags.Append(fmt.Errorf("error selecting workspace: %w", err))
   184  		return nil, diags
   185  	}
   186  
   187  	// Get the latest state snapshot from the backend for the current workspace
   188  	stateFile, stateErr := getStateFromBackend(b, workspace)
   189  	if stateErr != nil {
   190  		diags = diags.Append(stateErr)
   191  		return nil, diags
   192  	}
   193  
   194  	return stateFile, diags
   195  }
   196  
   197  func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) {
   198  	var diags tfdiags.Diagnostics
   199  	var planErr, stateErr error
   200  	var plan *plans.Plan
   201  	var jsonPlan *cloudplan.RemotePlanJSON
   202  	var stateFile *statefile.File
   203  	var config *configs.Config
   204  
   205  	// Path might be a local plan file, a bookmark to a saved cloud plan, or a
   206  	// state file. First, try to get a plan and associated data from a local
   207  	// plan file. If that fails, try to get a json plan from the path argument.
   208  	// If that fails, try to get the statefile from the path argument.
   209  	plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc)
   210  	if planErr != nil {
   211  		stateFile, stateErr = getStateFromPath(path, enc)
   212  		if stateErr != nil {
   213  			// To avoid spamming the user with irrelevant errors, first check to
   214  			// see if one of our errors happens to know for a fact what file
   215  			// type we were dealing with. If so, then we can ignore the other
   216  			// ones (which are likely to be something unhelpful like "not a
   217  			// valid zip file"). If not, we can fall back to dumping whatever
   218  			// we've got.
   219  			var unLocal *planfile.ErrUnusableLocalPlan
   220  			var unState *statefile.ErrUnusableState
   221  			var unMisc *errUnusableDataMisc
   222  			if errors.As(planErr, &unLocal) {
   223  				diags = diags.Append(
   224  					tfdiags.Sourceless(
   225  						tfdiags.Error,
   226  						"Couldn't show local plan",
   227  						fmt.Sprintf("Plan read error: %s", unLocal),
   228  					),
   229  				)
   230  			} else if errors.As(planErr, &unMisc) {
   231  				diags = diags.Append(
   232  					tfdiags.Sourceless(
   233  						tfdiags.Error,
   234  						fmt.Sprintf("Couldn't show %s", unMisc.kind),
   235  						fmt.Sprintf("Plan read error: %s", unMisc),
   236  					),
   237  				)
   238  			} else if errors.As(stateErr, &unState) {
   239  				diags = diags.Append(
   240  					tfdiags.Sourceless(
   241  						tfdiags.Error,
   242  						"Couldn't show state file",
   243  						fmt.Sprintf("Plan read error: %s", unState),
   244  					),
   245  				)
   246  			} else if errors.As(stateErr, &unMisc) {
   247  				diags = diags.Append(
   248  					tfdiags.Sourceless(
   249  						tfdiags.Error,
   250  						fmt.Sprintf("Couldn't show %s", unMisc.kind),
   251  						fmt.Sprintf("Plan read error: %s", unMisc),
   252  					),
   253  				)
   254  			} else {
   255  				// Ok, give up and show the really big error
   256  				diags = diags.Append(
   257  					tfdiags.Sourceless(
   258  						tfdiags.Error,
   259  						"Failed to read the given file as a state or plan file",
   260  						fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr),
   261  					),
   262  				)
   263  			}
   264  
   265  			return nil, nil, nil, nil, diags
   266  		}
   267  	}
   268  	return plan, jsonPlan, stateFile, config, diags
   269  }
   270  
   271  // getPlanFromPath returns a plan, json plan, statefile, and config if the
   272  // user-supplied path points to either a local or cloud plan file. Note that
   273  // some of the return values will be nil no matter what; local plan files do not
   274  // yield a json plan, and cloud plans do not yield real plan/state/config
   275  // structs. An error generally suggests that the given path is either a
   276  // directory or a statefile.
   277  func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
   278  	var err error
   279  	var plan *plans.Plan
   280  	var jsonPlan *cloudplan.RemotePlanJSON
   281  	var stateFile *statefile.File
   282  	var config *configs.Config
   283  
   284  	pf, err := planfile.OpenWrapped(path, enc.Plan())
   285  	if err != nil {
   286  		return nil, nil, nil, nil, err
   287  	}
   288  
   289  	if lp, ok := pf.Local(); ok {
   290  		plan, stateFile, config, err = getDataFromPlanfileReader(lp)
   291  	} else if cp, ok := pf.Cloud(); ok {
   292  		redacted := c.viewType != arguments.ViewJSON
   293  		jsonPlan, err = c.getDataFromCloudPlan(cp, redacted, enc)
   294  	}
   295  
   296  	return plan, jsonPlan, stateFile, config, err
   297  }
   298  
   299  func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) {
   300  	// Set up the backend
   301  	b, backendDiags := c.Backend(nil, enc.State())
   302  	if backendDiags.HasErrors() {
   303  		return nil, errUnusable(backendDiags.Err(), "cloud plan")
   304  	}
   305  	// Cloud plans only work if we're cloud.
   306  	cl, ok := b.(*cloud.Cloud)
   307  	if !ok {
   308  		return nil, errUnusable(fmt.Errorf("can't show a saved cloud plan unless the current root module is connected to Terraform Cloud"), "cloud plan")
   309  	}
   310  
   311  	result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted)
   312  	if err != nil {
   313  		err = errUnusable(err, "cloud plan")
   314  	}
   315  	return result, err
   316  }
   317  
   318  // getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file.
   319  func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *statefile.File, *configs.Config, error) {
   320  	// Get plan
   321  	plan, err := planReader.ReadPlan()
   322  	if err != nil {
   323  		return nil, nil, nil, err
   324  	}
   325  
   326  	// Get statefile
   327  	stateFile, err := planReader.ReadStateFile()
   328  	if err != nil {
   329  		return nil, nil, nil, err
   330  	}
   331  
   332  	// Get config
   333  	config, diags := planReader.ReadConfig()
   334  	if diags.HasErrors() {
   335  		return nil, nil, nil, errUnusable(diags.Err(), "local plan")
   336  	}
   337  
   338  	return plan, stateFile, config, err
   339  }
   340  
   341  // getStateFromPath returns a statefile if the user-supplied path points to a statefile.
   342  func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File, error) {
   343  	file, err := os.Open(path)
   344  	if err != nil {
   345  		return nil, fmt.Errorf("Error loading statefile: %w", err)
   346  	}
   347  	defer file.Close()
   348  
   349  	var stateFile *statefile.File
   350  	stateFile, err = statefile.Read(file, enc.State())
   351  	if err != nil {
   352  		return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err)
   353  	}
   354  	return stateFile, nil
   355  }
   356  
   357  // getStateFromBackend returns the State for the current workspace, if available.
   358  func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, error) {
   359  	// Get the state store for the given workspace
   360  	stateStore, err := b.StateMgr(workspace)
   361  	if err != nil {
   362  		return nil, fmt.Errorf("Failed to load state manager: %w", err)
   363  	}
   364  
   365  	// Refresh the state store with the latest state snapshot from persistent storage
   366  	if err := stateStore.RefreshState(); err != nil {
   367  		return nil, fmt.Errorf("Failed to load state: %w", err)
   368  	}
   369  
   370  	// Get the latest state snapshot and return it
   371  	stateFile := statemgr.Export(stateStore)
   372  	return stateFile, nil
   373  }