mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
68558ccd54
This commit extracts the remaining UI logic from the local backend, and removes access to the direct CLI output. This is replaced with an instance of a `views.Operation` interface, which codifies the current requirements for the local backend to interact with the user. The exception to this at present is interactivity: approving a plan still depends on the `UIIn` field for the backend. This is out of scope for this commit and can be revisited separately, at which time the `UIOut` field can also be removed. Changes in support of this: - Some instances of direct error output have been replaced with diagnostics, most notably in the emergency state backup handler. This requires reformatting the error messages to allow the diagnostic renderer to line-wrap them; - The "in-automation" logic has moved out of the backend and into the view implementation; - The plan, apply, refresh, and import commands instantiate a view and set it on the `backend.Operation` struct, as these are the only code paths which call the `local.Operation()` method that requires it; - The show command requires the plan rendering code which is now in the views package, so there is a stub implementation of a `views.Show` interface there. Other refactoring work in support of migrating these commands to the common views code structure will come in follow-up PRs, at which point we will be able to remove the UI instances from the unit tests for those commands.
261 lines
6.8 KiB
Go
261 lines
6.8 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/command/arguments"
|
|
"github.com/hashicorp/terraform/command/format"
|
|
"github.com/hashicorp/terraform/command/jsonplan"
|
|
"github.com/hashicorp/terraform/command/jsonstate"
|
|
"github.com/hashicorp/terraform/command/views"
|
|
"github.com/hashicorp/terraform/plans"
|
|
"github.com/hashicorp/terraform/plans/planfile"
|
|
"github.com/hashicorp/terraform/states/statefile"
|
|
"github.com/hashicorp/terraform/states/statemgr"
|
|
"github.com/hashicorp/terraform/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(args []string) int {
|
|
args = c.Meta.process(args)
|
|
cmdFlags := c.Meta.defaultFlagSet("show")
|
|
var jsonOutput bool
|
|
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
|
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
if err := cmdFlags.Parse(args); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
|
|
return 1
|
|
}
|
|
|
|
args = cmdFlags.Args()
|
|
if len(args) > 2 {
|
|
c.Ui.Error(
|
|
"The show command expects at most two arguments.\n The path to a " +
|
|
"Terraform state or plan file, and optionally -json for json output.\n")
|
|
cmdFlags.Usage()
|
|
return 1
|
|
}
|
|
|
|
// Check for user-supplied plugin path
|
|
var err error
|
|
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
|
|
return 1
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Load the backend
|
|
b, backendDiags := c.Backend(nil)
|
|
diags = diags.Append(backendDiags)
|
|
if backendDiags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// We require a local backend
|
|
local, ok := b.(backend.Local)
|
|
if !ok {
|
|
c.showDiagnostics(diags) // in case of any warnings in here
|
|
c.Ui.Error(ErrUnsupportedLocalOp)
|
|
return 1
|
|
}
|
|
|
|
// This is a read-only command
|
|
c.ignoreRemoteBackendVersionConflict(b)
|
|
|
|
// the show command expects the config dir to always be the cwd
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error getting cwd: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Determine if a planfile was passed to the command
|
|
var planFile *planfile.Reader
|
|
if len(args) > 0 {
|
|
// We will handle error checking later on - this is just required to
|
|
// load the local context if the given path is successfully read as
|
|
// a planfile.
|
|
planFile, _ = c.PlanFile(args[0])
|
|
}
|
|
|
|
// Build the operation
|
|
opReq := c.Operation(b)
|
|
opReq.ConfigDir = cwd
|
|
opReq.PlanFile = planFile
|
|
opReq.ConfigLoader, err = c.initConfigLoader()
|
|
opReq.AllowUnsetVariables = true
|
|
if err != nil {
|
|
diags = diags.Append(err)
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Get the context
|
|
ctx, _, ctxDiags := local.Context(opReq)
|
|
diags = diags.Append(ctxDiags)
|
|
if ctxDiags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Get the schemas from the context
|
|
schemas := ctx.Schemas()
|
|
|
|
var planErr, stateErr error
|
|
var plan *plans.Plan
|
|
var stateFile *statefile.File
|
|
|
|
// if a path was provided, try to read it as a path to a planfile
|
|
// if that fails, try to read the cli argument as a path to a statefile
|
|
if len(args) > 0 {
|
|
path := args[0]
|
|
plan, stateFile, planErr = getPlanFromPath(path)
|
|
if planErr != nil {
|
|
stateFile, stateErr = getStateFromPath(path)
|
|
if stateErr != nil {
|
|
c.Ui.Error(fmt.Sprintf(
|
|
"Terraform couldn't read the given file as a state or plan file.\n"+
|
|
"The errors while attempting to read the file as each format are\n"+
|
|
"shown below.\n\n"+
|
|
"State read error: %s\n\nPlan read error: %s",
|
|
stateErr,
|
|
planErr))
|
|
return 1
|
|
}
|
|
}
|
|
} else {
|
|
env, err := c.Workspace()
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
|
|
return 1
|
|
}
|
|
stateFile, stateErr = getStateFromEnv(b, env)
|
|
if stateErr != nil {
|
|
c.Ui.Error(stateErr.Error())
|
|
return 1
|
|
}
|
|
}
|
|
|
|
if plan != nil {
|
|
if jsonOutput {
|
|
config := ctx.Config()
|
|
jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas)
|
|
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Failed to marshal plan to json: %s", err))
|
|
return 1
|
|
}
|
|
c.Ui.Output(string(jsonPlan))
|
|
return 0
|
|
}
|
|
|
|
view := views.NewShow(arguments.ViewHuman, c.View)
|
|
view.Plan(plan, stateFile.State, schemas)
|
|
return 0
|
|
}
|
|
|
|
if jsonOutput {
|
|
// At this point, it is possible that there is neither state nor a plan.
|
|
// That's ok, we'll just return an empty object.
|
|
jsonState, err := jsonstate.Marshal(stateFile, schemas)
|
|
if err != nil {
|
|
c.Ui.Error(fmt.Sprintf("Failed to marshal state to json: %s", err))
|
|
return 1
|
|
}
|
|
c.Ui.Output(string(jsonState))
|
|
} else {
|
|
if stateFile == nil {
|
|
c.Ui.Output("No state.")
|
|
return 0
|
|
}
|
|
c.Ui.Output(format.State(&format.StateOpts{
|
|
State: stateFile.State,
|
|
Color: c.Colorize(),
|
|
Schemas: schemas,
|
|
}))
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (c *ShowCommand) Help() string {
|
|
helpText := `
|
|
Usage: terraform 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"
|
|
}
|
|
|
|
// getPlanFromPath returns a plan and statefile if the user-supplied path points
|
|
// to a planfile. 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, error) {
|
|
pr, err := planfile.Open(path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
plan, err := pr.ReadPlan()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
stateFile, err := pr.ReadStateFile()
|
|
return plan, stateFile, err
|
|
}
|
|
|
|
// getStateFromPath returns a statefile if the user-supplied path points to a statefile.
|
|
func getStateFromPath(path string) (*statefile.File, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error loading statefile: %s", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
var stateFile *statefile.File
|
|
stateFile, err = statefile.Read(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading %s as a statefile: %s", path, err)
|
|
}
|
|
return stateFile, nil
|
|
}
|
|
|
|
// getStateFromEnv returns the State for the current workspace, if available.
|
|
func getStateFromEnv(b backend.Backend, env string) (*statefile.File, error) {
|
|
// Get the state
|
|
stateStore, err := b.StateMgr(env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to load state manager: %s", err)
|
|
}
|
|
|
|
if err := stateStore.RefreshState(); err != nil {
|
|
return nil, fmt.Errorf("Failed to load state: %s", err)
|
|
}
|
|
|
|
sf := statemgr.Export(stateStore)
|
|
|
|
return sf, nil
|
|
}
|