| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "fmt" |
| "os" |
| "strings" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "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/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" |
| ) |
| |
| // ShowCommand is a Command implementation that reads and outputs the |
| // contents of a Terraform plan or state file. |
| type ShowCommand struct { |
| Meta |
| } |
| |
| func (c *ShowCommand) Run(rawArgs []string) int { |
| // Parse and apply global view arguments |
| common, rawArgs := arguments.ParseView(rawArgs) |
| c.View.Configure(common) |
| |
| // Parse and validate flags |
| args, diags := arguments.ParseShow(rawArgs) |
| if diags.HasErrors() { |
| c.View.Diagnostics(diags) |
| c.View.HelpPrompt("show") |
| return 1 |
| } |
| |
| // Set up view |
| view := views.NewShow(args.ViewType, c.View) |
| |
| // Check for user-supplied plugin path |
| var err error |
| 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, 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, 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, *statefile.File, *configs.Config, *terraform.Schemas, tfdiags.Diagnostics) { |
| var diags, showDiags tfdiags.Diagnostics |
| var plan *plans.Plan |
| 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, stateFile, config, schemas, diags |
| } |
| } |
| |
| // 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, stateFile, config, showDiags = c.showFromPath(path) |
| diags = diags.Append(showDiags) |
| if showDiags.HasErrors() { |
| return plan, stateFile, config, schemas, diags |
| } |
| } |
| |
| // Get schemas, if possible |
| if config != nil || stateFile != nil { |
| schemas, diags = c.MaybeGetSchemas(stateFile.State, config) |
| if diags.HasErrors() { |
| return plan, stateFile, config, schemas, diags |
| } |
| } |
| |
| return plan, stateFile, config, schemas, diags |
| } |
| func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // Load the backend |
| b, backendDiags := c.Backend(nil) |
| 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 |
| } |
| |
| // 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 |
| } |
| |
| func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| var planErr, stateErr error |
| var plan *plans.Plan |
| var stateFile *statefile.File |
| var config *configs.Config |
| |
| // Try to get the plan file and associated data from |
| // the path argument. If that fails, try to get the |
| // statefile from the path argument. |
| plan, stateFile, config, planErr = getPlanFromPath(path) |
| if planErr != nil { |
| stateFile, stateErr = getStateFromPath(path) |
| if stateErr != nil { |
| 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, diags |
| } |
| } |
| return plan, stateFile, config, diags |
| } |
| |
| // getPlanFromPath returns a plan, statefile, and config if the user-supplied |
| // path points to a plan file. If both plan and error are nil, the path is likely |
| // a directory. An error could suggest that the given path points to a statefile. |
| func getPlanFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, error) { |
| planReader, err := planfile.Open(path) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| |
| // 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 := planReader.ReadConfig() |
| if diags.HasErrors() { |
| return nil, nil, nil, diags.Err() |
| } |
| |
| return plan, stateFile, config, err |
| } |
| |
| // 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: %s", 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: %s", 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, err := b.StateMgr(workspace) |
| if err != nil { |
| return nil, fmt.Errorf("Failed to load state manager: %s", 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: %s", err) |
| } |
| |
| // Get the latest state snapshot and return it |
| stateFile := statemgr.Export(stateStore) |
| return stateFile, nil |
| } |