Merge pull request #30344 from hashicorp/lafentres/refactor-show-command
cli: Refactor show command & migrate to command arguments and views
This commit is contained in:
commit
6dcf00aefc
|
@ -275,13 +275,6 @@ type Operation struct {
|
|||
// the variables set in the plan are used instead, and they must be valid.
|
||||
AllowUnsetVariables bool
|
||||
|
||||
// When loading a plan file for a read-only operation, we may want to
|
||||
// disable the state lineage checks which are only relevant for operations
|
||||
// which can modify state. An example where this is important is showing
|
||||
// a plan which was prepared against a non-default state file, because the
|
||||
// lineage checks are always against the default state.
|
||||
DisablePlanFileStateLineageChecks bool
|
||||
|
||||
// View implements the logic for all UI interactions.
|
||||
View views.Operation
|
||||
|
||||
|
|
|
@ -284,7 +284,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader,
|
|||
))
|
||||
return nil, snap, diags
|
||||
}
|
||||
if !op.DisablePlanFileStateLineageChecks && currentStateMeta != nil {
|
||||
if currentStateMeta != nil {
|
||||
// If the caller sets this, we require that the stored prior state
|
||||
// has the same metadata, which is an extra safety check that nothing
|
||||
// has changed since the plan was created. (All of the "real-world"
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Show represents the command-line arguments for the show command.
|
||||
type Show struct {
|
||||
// Path is the path to the state file or plan file to be displayed. If
|
||||
// unspecified, show will display the latest state snapshot.
|
||||
Path string
|
||||
|
||||
// ViewType specifies which output format to use: human, JSON, or "raw".
|
||||
ViewType ViewType
|
||||
}
|
||||
|
||||
// ParseShow processes CLI arguments, returning a Show value and errors.
|
||||
// If errors are encountered, a Show value is still returned representing
|
||||
// the best effort interpretation of the arguments.
|
||||
func ParseShow(args []string) (*Show, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
show := &Show{
|
||||
Path: "",
|
||||
}
|
||||
|
||||
var jsonOutput bool
|
||||
cmdFlags := defaultFlagSet("show")
|
||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
if len(args) > 1 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Too many command line arguments",
|
||||
"Expected at most one positional argument.",
|
||||
))
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
show.Path = args[0]
|
||||
}
|
||||
|
||||
switch {
|
||||
case jsonOutput:
|
||||
show.ViewType = ViewJSON
|
||||
default:
|
||||
show.ViewType = ViewHuman
|
||||
}
|
||||
|
||||
return show, diags
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseShow_valid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Show
|
||||
}{
|
||||
"defaults": {
|
||||
nil,
|
||||
&Show{
|
||||
Path: "",
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
},
|
||||
"json": {
|
||||
[]string{"-json"},
|
||||
&Show{
|
||||
Path: "",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
},
|
||||
"path": {
|
||||
[]string{"-json", "foo"},
|
||||
&Show{
|
||||
Path: "foo",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseShow(tc.args)
|
||||
if len(diags) > 0 {
|
||||
t.Fatalf("unexpected diags: %v", diags)
|
||||
}
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseShow_invalid(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
args []string
|
||||
want *Show
|
||||
wantDiags tfdiags.Diagnostics
|
||||
}{
|
||||
"unknown flag": {
|
||||
[]string{"-boop"},
|
||||
&Show{
|
||||
Path: "",
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
"flag provided but not defined: -boop",
|
||||
),
|
||||
},
|
||||
},
|
||||
"too many arguments": {
|
||||
[]string{"-json", "bar", "baz"},
|
||||
&Show{
|
||||
Path: "bar",
|
||||
ViewType: ViewJSON,
|
||||
},
|
||||
tfdiags.Diagnostics{
|
||||
tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Too many command line arguments",
|
||||
"Expected at most one positional argument.",
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, gotDiags := ParseShow(tc.args)
|
||||
if *got != *tc.want {
|
||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want)
|
||||
}
|
||||
if !reflect.DeepEqual(gotDiags, tc.wantDiags) {
|
||||
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -7,14 +7,13 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -24,174 +23,40 @@ 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()))
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
// 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 {
|
||||
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
|
||||
diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// Load the backend
|
||||
b, backendDiags := c.Backend(nil)
|
||||
diags = diags.Append(backendDiags)
|
||||
if backendDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.ignoreRemoteVersionConflict(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
|
||||
opReq.DisablePlanFileStateLineageChecks = true
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the context
|
||||
lr, _, ctxDiags := local.LocalRun(opReq)
|
||||
diags = diags.Append(ctxDiags)
|
||||
if ctxDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the schemas from the context
|
||||
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
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 := lr.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, 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
|
||||
// Display the data
|
||||
return view.Display(config, plan, stateFile, schemas)
|
||||
}
|
||||
|
||||
func (c *ShowCommand) Help() string {
|
||||
|
@ -215,52 +80,171 @@ 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
|
||||
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, err := pr.ReadPlan()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
stateFile, err := pr.ReadStateFile()
|
||||
return plan, stateFile, err
|
||||
// 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 {
|
||||
opts, err := c.contextOpts()
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
return plan, stateFile, config, schemas, diags
|
||||
}
|
||||
tfCtx, ctxDiags := terraform.NewContext(opts)
|
||||
diags = diags.Append(ctxDiags)
|
||||
if ctxDiags.HasErrors() {
|
||||
return plan, stateFile, config, schemas, diags
|
||||
}
|
||||
var schemaDiags tfdiags.Diagnostics
|
||||
schemas, schemaDiags = tfCtx.Schemas(config, stateFile.State)
|
||||
diags = diags.Append(schemaDiags)
|
||||
if schemaDiags.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.Error())
|
||||
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) {
|
||||
f, err := os.Open(path)
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error loading statefile: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
defer file.Close()
|
||||
|
||||
var stateFile *statefile.File
|
||||
stateFile, err = statefile.Read(f)
|
||||
stateFile, err = statefile.Read(file)
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
sf := statemgr.Export(stateStore)
|
||||
|
||||
return sf, nil
|
||||
// Get the latest state snapshot and return it
|
||||
stateFile := statemgr.Export(stateStore)
|
||||
return stateFile, nil
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package command
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -22,13 +21,11 @@ import (
|
|||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestShow(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
func TestShow_badArgs(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
@ -36,40 +33,99 @@ func TestShow(t *testing.T) {
|
|||
args := []string{
|
||||
"bad",
|
||||
"bad",
|
||||
"-no-color",
|
||||
}
|
||||
if code := c.Run(args); code != 1 {
|
||||
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
||||
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_noArgs(t *testing.T) {
|
||||
func TestShow_noArgsNoState(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
code := c.Run([]string{})
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
got := output.Stdout()
|
||||
want := `No state.`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_noArgsWithState(t *testing.T) {
|
||||
// Get a temp cwd
|
||||
tmp, cwd := testCwd(t)
|
||||
defer testFixCwd(t, tmp, cwd)
|
||||
// Create the default state
|
||||
testStateFileDefault(t, testState())
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
if code := c.Run([]string{}); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
||||
code := c.Run([]string{})
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if !strings.Contains(ui.OutputWriter.String(), "# test_instance.foo:") {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
got := output.Stdout()
|
||||
want := `# test_instance.foo:`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_argsWithState(t *testing.T) {
|
||||
// Create the default state
|
||||
statePath := testStateFile(t, testState())
|
||||
stateDir := filepath.Dir(statePath)
|
||||
defer os.RemoveAll(stateDir)
|
||||
defer testChdir(t, stateDir)()
|
||||
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
path := filepath.Base(statePath)
|
||||
args := []string{
|
||||
path,
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/hashicorp/terraform/issues/21462
|
||||
func TestShow_aliasedProvider(t *testing.T) {
|
||||
func TestShow_argsWithStateAliasedProvider(t *testing.T) {
|
||||
// Create the default state with aliased resource
|
||||
testState := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
|
@ -95,103 +151,198 @@ func TestShow_aliasedProvider(t *testing.T) {
|
|||
defer os.RemoveAll(stateDir)
|
||||
defer testChdir(t, stateDir)()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
// the statefile created by testStateFile is named state.tfstate
|
||||
args := []string{"state.tfstate"}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad exit code: \n%s", ui.OutputWriter.String())
|
||||
path := filepath.Base(statePath)
|
||||
args := []string{
|
||||
path,
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if strings.Contains(ui.OutputWriter.String(), "# missing schema for provider \"test.alias\"") {
|
||||
t.Fatalf("bad output: \n%s", ui.OutputWriter.String())
|
||||
got := output.Stdout()
|
||||
want := `# missing schema for provider \"test.alias\"`
|
||||
if strings.Contains(got, want) {
|
||||
t.Fatalf("unexpected output\ngot: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_noArgsNoState(t *testing.T) {
|
||||
// Create the default state
|
||||
statePath := testStateFile(t, testState())
|
||||
stateDir := filepath.Dir(statePath)
|
||||
defer os.RemoveAll(stateDir)
|
||||
defer testChdir(t, stateDir)()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
func TestShow_argsPlanFileDoesNotExist(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
// the statefile created by testStateFile is named state.tfstate
|
||||
args := []string{"state.tfstate"}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
|
||||
args := []string{
|
||||
"doesNotExist.tfplan",
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout())
|
||||
}
|
||||
|
||||
got := output.Stderr()
|
||||
want := `Plan read error: open doesNotExist.tfplan:`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_argsStatefileDoesNotExist(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"doesNotExist.tfstate",
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout())
|
||||
}
|
||||
|
||||
got := output.Stderr()
|
||||
want := `State read error: Error loading statefile:`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_json_argsPlanFileDoesNotExist(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-json",
|
||||
"doesNotExist.tfplan",
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout())
|
||||
}
|
||||
|
||||
got := output.Stderr()
|
||||
want := `Plan read error: open doesNotExist.tfplan:`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_json_argsStatefileDoesNotExist(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-json",
|
||||
"doesNotExist.tfstate",
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 1 {
|
||||
t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout())
|
||||
}
|
||||
|
||||
got := output.Stderr()
|
||||
want := `State read error: Error loading statefile:`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_planNoop(t *testing.T) {
|
||||
planPath := testPlanFileNoop(t)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
planPath,
|
||||
"-no-color",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
got := output.Stdout()
|
||||
want := `No changes. Your infrastructure matches the configuration.`
|
||||
got := done(t).Stdout()
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
|
||||
t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow_planWithChanges(t *testing.T) {
|
||||
planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
planPathWithChanges,
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
got := output.Stdout()
|
||||
want := `test_instance.foo must be replaced`
|
||||
got := done(t).Stdout()
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,30 +390,34 @@ func TestShow_planWithForceReplaceChange(t *testing.T) {
|
|||
plan,
|
||||
)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
planFilePath,
|
||||
"-no-color",
|
||||
}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
got := output.Stdout()
|
||||
want := `test_instance.foo will be replaced, as requested`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
||||
got := done(t).Stdout()
|
||||
if want := `test_instance.foo will be replaced, as requested`; !strings.Contains(got, want) {
|
||||
t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
|
||||
}
|
||||
if want := `Plan: 1 to add, 0 to change, 1 to destroy.`; !strings.Contains(got, want) {
|
||||
t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
|
||||
want = `Plan: 1 to add, 0 to change, 1 to destroy.`
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -270,12 +425,10 @@ func TestShow_planWithForceReplaceChange(t *testing.T) {
|
|||
func TestShow_plan_json(t *testing.T) {
|
||||
planPath := showFixturePlanFile(t, plans.Create)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
@ -283,9 +436,13 @@ func TestShow_plan_json(t *testing.T) {
|
|||
args := []string{
|
||||
"-json",
|
||||
planPath,
|
||||
"-no-color",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,21 +451,23 @@ func TestShow_state(t *testing.T) {
|
|||
statePath := testStateFile(t, originalState)
|
||||
defer os.RemoveAll(filepath.Dir(statePath))
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
statePath,
|
||||
"-no-color",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,18 +498,15 @@ func TestShow_json_output(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
p := showFixtureProvider()
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
m := Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
}
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
ic := &InitCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
if expectError {
|
||||
|
@ -360,22 +516,35 @@ func TestShow_json_output(t *testing.T) {
|
|||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
// plan
|
||||
planView, planDone := testView(t)
|
||||
pc := &PlanCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: planView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-out=terraform.plan",
|
||||
}
|
||||
|
||||
if code := pc.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
code := pc.Run(args)
|
||||
planOutput := planDone(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr())
|
||||
}
|
||||
|
||||
// flush the plan output from the mock ui
|
||||
ui.OutputWriter.Reset()
|
||||
// show
|
||||
showView, showDone := testView(t)
|
||||
sc := &ShowCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: showView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
|
||||
args = []string{
|
||||
|
@ -383,25 +552,27 @@ func TestShow_json_output(t *testing.T) {
|
|||
"terraform.plan",
|
||||
}
|
||||
defer os.Remove("terraform.plan")
|
||||
code = sc.Run(args)
|
||||
showOutput := showDone(t)
|
||||
|
||||
if code := sc.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr())
|
||||
}
|
||||
|
||||
// compare ui output to wanted output
|
||||
// compare view output to wanted output
|
||||
var got, want plan
|
||||
|
||||
gotString := ui.OutputWriter.String()
|
||||
gotString := showOutput.Stdout()
|
||||
json.Unmarshal([]byte(gotString), &got)
|
||||
|
||||
wantFile, err := os.Open("output.json")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
defer wantFile.Close()
|
||||
byteValue, err := ioutil.ReadAll(wantFile)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
json.Unmarshal([]byte(byteValue), &want)
|
||||
|
||||
|
@ -423,43 +594,48 @@ func TestShow_json_output_sensitive(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
p := showFixtureSensitiveProvider()
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
m := Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
}
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
ic := &InitCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
// flush init output
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// plan
|
||||
planView, planDone := testView(t)
|
||||
pc := &PlanCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: planView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-out=terraform.plan",
|
||||
}
|
||||
code := pc.Run(args)
|
||||
planOutput := planDone(t)
|
||||
|
||||
if code := pc.Run(args); code != 0 {
|
||||
fmt.Println(ui.OutputWriter.String())
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr())
|
||||
}
|
||||
|
||||
// flush the plan output from the mock ui
|
||||
ui.OutputWriter.Reset()
|
||||
// show
|
||||
showView, showDone := testView(t)
|
||||
sc := &ShowCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: showView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
|
||||
args = []string{
|
||||
|
@ -467,25 +643,27 @@ func TestShow_json_output_sensitive(t *testing.T) {
|
|||
"terraform.plan",
|
||||
}
|
||||
defer os.Remove("terraform.plan")
|
||||
code = sc.Run(args)
|
||||
showOutput := showDone(t)
|
||||
|
||||
if code := sc.Run(args); code != 0 {
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr())
|
||||
}
|
||||
|
||||
// compare ui output to wanted output
|
||||
var got, want plan
|
||||
|
||||
gotString := ui.OutputWriter.String()
|
||||
gotString := showOutput.Stdout()
|
||||
json.Unmarshal([]byte(gotString), &got)
|
||||
|
||||
wantFile, err := os.Open("output.json")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
defer wantFile.Close()
|
||||
byteValue, err := ioutil.ReadAll(wantFile)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
json.Unmarshal([]byte(byteValue), &want)
|
||||
|
||||
|
@ -520,31 +698,35 @@ func TestShow_json_output_state(t *testing.T) {
|
|||
defer close()
|
||||
|
||||
p := showFixtureProvider()
|
||||
ui := new(cli.MockUi)
|
||||
view, _ := testView(t)
|
||||
m := Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
ProviderSource: providerSource,
|
||||
}
|
||||
|
||||
// init
|
||||
ui := new(cli.MockUi)
|
||||
ic := &InitCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
Ui: ui,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
if code := ic.Run([]string{}); code != 0 {
|
||||
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
// flush the plan output from the mock ui
|
||||
ui.OutputWriter.Reset()
|
||||
// show
|
||||
showView, showDone := testView(t)
|
||||
sc := &ShowCommand{
|
||||
Meta: m,
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(p),
|
||||
View: showView,
|
||||
ProviderSource: providerSource,
|
||||
},
|
||||
}
|
||||
|
||||
if code := sc.Run([]string{"-json"}); code != 0 {
|
||||
t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
|
||||
code := sc.Run([]string{"-json"})
|
||||
showOutput := showDone(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr())
|
||||
}
|
||||
|
||||
// compare ui output to wanted output
|
||||
|
@ -556,17 +738,17 @@ func TestShow_json_output_state(t *testing.T) {
|
|||
}
|
||||
var got, want state
|
||||
|
||||
gotString := ui.OutputWriter.String()
|
||||
gotString := showOutput.Stdout()
|
||||
json.Unmarshal([]byte(gotString), &got)
|
||||
|
||||
wantFile, err := os.Open("output.json")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
defer wantFile.Close()
|
||||
byteValue, err := ioutil.ReadAll(wantFile)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
t.Fatalf("unexpected err: %s", err)
|
||||
}
|
||||
json.Unmarshal([]byte(byteValue), &want)
|
||||
|
||||
|
@ -599,27 +781,29 @@ func TestShow_planWithNonDefaultStateLineage(t *testing.T) {
|
|||
}
|
||||
planPath := testPlanFileMatchState(t, snap, state, plan, stateMeta)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &ShowCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: metaOverridesForProvider(testProvider()),
|
||||
Ui: ui,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
planPath,
|
||||
"-no-color",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
|
||||
if code != 0 {
|
||||
t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr())
|
||||
}
|
||||
|
||||
got := output.Stdout()
|
||||
want := `No changes. Your infrastructure matches the configuration.`
|
||||
got := done(t).Stdout()
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"format_version": "1.0"
|
||||
}
|
|
@ -2,37 +2,95 @@ package views
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/format"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
||||
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// FIXME: this is a temporary partial definition of the view for the show
|
||||
// command, in place to allow access to the plan renderer which is now in the
|
||||
// views package.
|
||||
type Show interface {
|
||||
Plan(plan *plans.Plan, schemas *terraform.Schemas)
|
||||
// Display renders the plan, if it is available. If plan is nil, it renders the statefile.
|
||||
Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int
|
||||
|
||||
// Diagnostics renders early diagnostics, resulting from argument parsing.
|
||||
Diagnostics(diags tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// FIXME: the show view should support both human and JSON types. This code is
|
||||
// currently only used to render the plan in human-readable UI, so does not yet
|
||||
// support JSON.
|
||||
func NewShow(vt arguments.ViewType, view *View) Show {
|
||||
switch vt {
|
||||
case arguments.ViewJSON:
|
||||
return &ShowJSON{view: view}
|
||||
case arguments.ViewHuman:
|
||||
return &ShowHuman{View: *view}
|
||||
return &ShowHuman{view: view}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown view type %v", vt))
|
||||
}
|
||||
}
|
||||
|
||||
type ShowHuman struct {
|
||||
View
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Show = (*ShowHuman)(nil)
|
||||
|
||||
func (v *ShowHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) {
|
||||
renderPlan(plan, schemas, &v.View)
|
||||
func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int {
|
||||
if plan != nil {
|
||||
renderPlan(plan, schemas, v.view)
|
||||
} else {
|
||||
if stateFile == nil {
|
||||
v.view.streams.Println("No state.")
|
||||
return 0
|
||||
}
|
||||
|
||||
v.view.streams.Println(format.State(&format.StateOpts{
|
||||
State: stateFile.State,
|
||||
Color: v.view.colorize,
|
||||
Schemas: schemas,
|
||||
}))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
type ShowJSON struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
var _ Show = (*ShowJSON)(nil)
|
||||
|
||||
func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int {
|
||||
if plan != nil {
|
||||
jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas)
|
||||
|
||||
if err != nil {
|
||||
v.view.streams.Eprintf("Failed to marshal plan to json: %s", err)
|
||||
return 1
|
||||
}
|
||||
v.view.streams.Println(string(jsonPlan))
|
||||
} else {
|
||||
// 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 {
|
||||
v.view.streams.Eprintf("Failed to marshal state to json: %s", err)
|
||||
return 1
|
||||
}
|
||||
v.view.streams.Println(string(jsonState))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Diagnostics should only be called if show cannot be executed.
|
||||
// In this case, we choose to render human-readable diagnostic output,
|
||||
// primarily for backwards compatibility.
|
||||
func (v *ShowJSON) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/initwd"
|
||||
"github.com/hashicorp/terraform/internal/plans"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/states/statefile"
|
||||
"github.com/hashicorp/terraform/internal/terminal"
|
||||
"github.com/hashicorp/terraform/internal/terraform"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestShowHuman(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
plan *plans.Plan
|
||||
stateFile *statefile.File
|
||||
schemas *terraform.Schemas
|
||||
wantExact bool
|
||||
wantString string
|
||||
}{
|
||||
"plan file": {
|
||||
testPlan(t),
|
||||
nil,
|
||||
testSchemas(),
|
||||
false,
|
||||
"# test_resource.foo will be created",
|
||||
},
|
||||
"statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: testState(),
|
||||
},
|
||||
testSchemas(),
|
||||
false,
|
||||
"# test_resource.foo:",
|
||||
},
|
||||
"empty statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: states.NewState(),
|
||||
},
|
||||
testSchemas(),
|
||||
true,
|
||||
"\n",
|
||||
},
|
||||
"nothing": {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
"No state.\n",
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewShow(arguments.ViewHuman, view)
|
||||
|
||||
code := v.Display(nil, testCase.plan, testCase.stateFile, testCase.schemas)
|
||||
if code != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", code)
|
||||
}
|
||||
|
||||
output := done(t)
|
||||
got := output.Stdout()
|
||||
want := testCase.wantString
|
||||
if (testCase.wantExact && got != want) || (!testCase.wantExact && !strings.Contains(got, want)) {
|
||||
t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
plan *plans.Plan
|
||||
stateFile *statefile.File
|
||||
}{
|
||||
"plan file": {
|
||||
testPlan(t),
|
||||
nil,
|
||||
},
|
||||
"statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: testState(),
|
||||
},
|
||||
},
|
||||
"empty statefile": {
|
||||
nil,
|
||||
&statefile.File{
|
||||
Serial: 0,
|
||||
Lineage: "fake-for-testing",
|
||||
State: states.NewState(),
|
||||
},
|
||||
},
|
||||
"nothing": {
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show")
|
||||
defer configCleanup()
|
||||
|
||||
for name, testCase := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
streams, done := terminal.StreamsForTesting(t)
|
||||
view := NewView(streams)
|
||||
view.Configure(&arguments.View{NoColor: true})
|
||||
v := NewShow(arguments.ViewJSON, view)
|
||||
|
||||
schemas := &terraform.Schemas{
|
||||
Providers: map[addrs.Provider]*terraform.ProviderSchema{
|
||||
addrs.NewDefaultProvider("test"): {
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_resource": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"foo": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
code := v.Display(config, testCase.plan, testCase.stateFile, schemas)
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("expected 0 return code, got %d", code)
|
||||
}
|
||||
|
||||
// Make sure the result looks like JSON; we comprehensively test
|
||||
// the structure of this output in the command package tests.
|
||||
var result map[string]interface{}
|
||||
got := done(t).All()
|
||||
t.Logf("output: %s", got)
|
||||
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testState returns a test State structure.
|
||||
func testState() *states.State {
|
||||
return states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_resource",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte(`{"id":"bar","foo":"value"}`),
|
||||
Status: states.ObjectReady,
|
||||
},
|
||||
addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider("test"),
|
||||
Module: addrs.RootModule,
|
||||
},
|
||||
)
|
||||
// DeepCopy is used here to ensure our synthetic state matches exactly
|
||||
// with a state that will have been copied during the command
|
||||
// operation, and all fields have been copied correctly.
|
||||
}).DeepCopy()
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "test_resource" "foo" {
|
||||
foo = "value"
|
||||
}
|
Loading…
Reference in New Issue