terraform/command/show.go

267 lines
7.4 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"
localBackend "github.com/hashicorp/terraform/backend/local"
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 = c.Meta.process(args)
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 {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
2014-07-13 04:47:31 +02:00
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
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
}
// 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
}
2014-07-13 04:47:31 +02:00
}
} 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())
2017-01-19 05:50:45 +01:00
return 1
}
2014-07-13 04:47:31 +02:00
}
2014-07-13 04:47:31 +02:00
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
}
// FIXME: We currently call into the local backend for this, since
// the "terraform plan" logic lives there and our package call graph
// means we can't orient this dependency the other way around. In
// future we'll hopefully be able to refactor the backend architecture
// a little so that CLI UI rendering always happens in this "command"
// package rather than in the backends themselves, but for now we're
// accepting this oddity because "terraform show" is a less commonly
// used way to render a plan than "terraform plan" is.
backend/local: treat output changes as side-effects to be applied This is a baby-step towards an intended future where all Terraform actions which have side-effects in either remote objects or the Terraform state can go through the plan+apply workflow. This initial change is focused only on allowing plan+apply for changes to root module output values, so that these can be written into a new state snapshot (for consumption by terraform_remote_state elsewhere) without having to go outside of the primary workflow by running "terraform refresh". This is also better than "terraform refresh" because it gives an opportunity to review the proposed changes before applying them, as we're accustomed to with resource changes. The downside here is that Terraform Core was not designed to produce accurate changesets for root module outputs. Although we added a place for it in the plan model in Terraform 0.12, Terraform Core currently produces inaccurate changesets there which don't properly track the prior values. We're planning to rework Terraform Core's evaluation approach in a forthcoming release so it would itself be able to distinguish between the prior state and the planned new state to produce an accurate changeset, but this commit introduces a temporary stop-gap solution of implementing the logic up in the local backend code, where we can freeze a snapshot of the prior state before we take any other actions and then use that to produce an accurate output changeset to decide whether the plan has externally-visible side-effects and render any changes to output values. This temporary approach should be replaced by a more appropriately-placed solution in Terraform Core in a release, which should then allow further behaviors in similar vein, such as user-visible drift detection for resource instances.
2020-05-27 01:59:06 +02:00
// We're setting priorState to null because a saved plan file only
// records the base state (possibly updated by refresh), not the
// prior state (direct result of the previous apply).
localBackend.RenderPlan(plan, stateFile.State, nil, schemas, c.Ui, c.Colorize())
2014-07-13 04:47:31 +02:00
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,
}))
}
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 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
}