mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
8664749b59
Terraform Core expects all variables to be set, but for some ancillary commands it's fine for them to just be set to placeholders because the variable values themselves are not key to the command's functionality as long as the terraform.Context is still self-consistent. For such commands, rather than prompting for interactive input for required variables we'll just stub them out as unknowns to reflect that they are placeholders for values that a user would normally need to provide. This achieves a similar effect to how these commands behaved before, but without the tendency to produce a slightly invalid terraform.Context that would fail in strange ways when asked to run certain operations.
254 lines
6.5 KiB
Go
254 lines
6.5 KiB
Go
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/command/format"
|
|
"github.com/hashicorp/terraform/command/jsonplan"
|
|
"github.com/hashicorp/terraform/command/jsonstate"
|
|
"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, err := c.Meta.process(args, false)
|
|
if err != nil {
|
|
return 1
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
|
|
// 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 := c.Workspace()
|
|
stateFile, stateErr = getStateFromEnv(b, env)
|
|
if stateErr != nil {
|
|
c.Ui.Error(stateErr.Error())
|
|
return 1
|
|
}
|
|
}
|
|
|
|
if plan != nil {
|
|
if jsonOutput == true {
|
|
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
|
|
}
|
|
dispPlan := format.NewPlan(plan.Changes)
|
|
c.Ui.Output(dispPlan.Format(c.Colorize()))
|
|
return 0
|
|
}
|
|
|
|
if jsonOutput == true {
|
|
// 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 "Inspect Terraform state or 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
|
|
}
|