terraform/command/show.go

272 lines
7.1 KiB
Go
Raw Normal View History

2014-07-13 04:47:31 +02:00
package command
import (
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/backend"
2017-01-19 05:50:45 +01:00
"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"
2014-07-13 04:47:31 +02:00
)
// ShowCommand is a Command implementation that reads and outputs the
// contents of a Terraform plan or state file.
type ShowCommand struct {
Meta
2014-07-13 04:47:31 +02:00
}
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")
2014-07-13 04:47:31 +02:00
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
args = cmdFlags.Args()
if len(args) > 2 {
2014-07-13 04:47:31 +02:00
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")
2014-07-13 04:47:31 +02:00
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()
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, 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
}
2014-07-13 04:47:31 +02:00
}
}
2017-01-19 05:50:45 +01:00
if stateFile == nil {
env := c.Workspace()
stateFile, stateErr = getStateFromEnv(b, env)
if err != nil {
c.Ui.Error(err.Error())
2017-01-19 05:50:45 +01:00
return 1
}
2014-07-13 04:47:31 +02:00
}
// This is an odd-looking check, because it's ok if we have a plan and an
// empty state, and we've already validated that any command-line arguments
// have been read successfully
if plan == nil && stateFile == nil {
c.Ui.Output("No state.")
return 0
2014-07-13 04:47:31 +02:00
}
if plan != nil {
if jsonOutput == true {
config := ctx.Config()
var err error
var jsonPlan []byte
// If there is no prior state, we have all the schemas needed.
if stateFile == nil {
jsonPlan, err = jsonplan.Marshal(config, plan, stateFile, schemas, nil)
} else {
// If there is state, we need the state-specific schemas, which
// may differ from the schemas loaded from the plan.
// This occurs if there is a data_source in the state that was
// removed from the configuration, because terraform core does
// not need to load the schema to remove a data source.
opReq.PlanFile = nil
ctx, _, ctxDiags := local.Context(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
stateSchemas := ctx.Schemas()
jsonPlan, err = jsonplan.Marshal(config, plan, stateFile, schemas, stateSchemas)
}
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)
command/format: improve consistency of plan results Previously the rendered plan output was constructed directly from the core plan and then annotated with counts derived from the count hook. At various places we applied little adjustments to deal with the fact that the user-facing diff model is not identical to the internal diff model, including the special handling of data source reads and destroys. Since this logic was just muddled into the rendering code, it behaved inconsistently with the tally of adds, updates and deletes. This change reworks the plan formatter so that it happens in two stages: - First, we produce a specialized Plan object that is tailored for use in the UI. This applies all the relevant logic to transform the physical model into the user model. - Second, we do a straightforward visual rendering of the display-oriented plan object. For the moment this is slightly overkill since there's only one rendering path, but it does give us the benefit of letting the counts be derived from the same data as the full detailed diff, ensuring that they'll stay consistent. Later we may choose to have other UIs for plans, such as a machine-readable output intended to drive a web UI. In that case, we'd want the web UI to consume a serialization of the _display-oriented_ plan so that it doesn't need to re-implement all of these UI special cases. This introduces to core a new diff action type for "refresh". Currently this is used _only_ in the UI layer, to represent data source reads. Later it would be good to use this type for the core diff as well, to improve consistency, but that is left for another day to keep this change focused on the UI.
2017-08-24 01:23:02 +02:00
c.Ui.Output(dispPlan.Format(c.Colorize()))
2014-07-13 04:47:31 +02:00
return 0
}
if jsonOutput == true {
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 {
c.Ui.Output(format.State(&format.StateOpts{
State: stateFile.State,
Color: c.Colorize(),
Schemas: schemas,
}))
}
2014-07-13 04:47:31 +02:00
return 0
}
func (c *ShowCommand) Help() string {
helpText := `
2014-10-11 21:57:47 +02:00
Usage: terraform show [options] [path]
2014-07-13 04:47:31 +02:00
Reads and outputs a Terraform state or plan file in a human-readable
2014-10-11 21:57:47 +02:00
form. If no path is specified, the current state will be shown.
2014-07-13 04:47:31 +02:00
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.
2014-07-13 04:47:31 +02:00
`
return strings.TrimSpace(helpText)
}
func (c *ShowCommand) Synopsis() string {
return "Inspect Terraform state or plan"
}
// getPlanFromPath returns a plan 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, error) {
pr, err := planfile.Open(path)
if err != nil {
return nil, err
}
plan, err := pr.ReadPlan()
if err != nil {
return nil, err
}
return plan, nil
}
// 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)
}
sf := statemgr.Export(stateStore)
return sf, nil
}