Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

command/state: update and fix the state show command #19200

Merged
merged 1 commit into from
Nov 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion command/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ type ShowCommand struct {
}

func (c *ShowCommand) Run(args []string) int {

args, err := c.Meta.process(args, false)
if err != nil {
return 1
Expand Down
6 changes: 0 additions & 6 deletions command/state_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,3 @@ func (c *StateMeta) filter(state *states.State, args []string) ([]*states.Filter

return results, nil
}

const errStateMultiple = `Multiple instances found for the given pattern!

This command requires that the pattern match exactly one instance
of a resource. To view the matched instances, use "terraform state list".
Please modify the pattern to match only a single instance.`
4 changes: 2 additions & 2 deletions command/state_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func (c *StatePushCommand) Run(args []string) int {
args = cmdFlags.Args()

if len(args) != 1 {
c.Ui.Error("Exactly one argument expected: path to state to push")
return 1
c.Ui.Error("Exactly one argument expected.\n")
return cli.RunResultHelp
}

// Determine our reader for the input state. This is the filepath
Expand Down
145 changes: 89 additions & 56 deletions command/state_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package command

import (
"fmt"
"os"
"strings"

"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/states"
"github.com/mitchellh/cli"
)

Expand All @@ -20,11 +25,15 @@ func (c *StateShowCommand) Run(args []string) int {
}

cmdFlags := c.Meta.flagSet("state show")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error("Exactly one argument expected.\n")
return cli.RunResultHelp
}

// Load the backend
b, backendDiags := c.Backend(nil)
Expand All @@ -33,73 +42,85 @@ func (c *StateShowCommand) Run(args []string) int {
return 1
}

// We require a local backend
local, ok := b.(backend.Local)
if !ok {
c.Ui.Error(ErrUnsupportedLocalOp)
return 1
}

// Check if the address can be parsed
addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0])
if addrDiags.HasErrors() {
c.Ui.Error(fmt.Sprintf(errParsingAddress, args[0]))
return 1
}

// We expect 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
}

// Build the operation (required to get the schemas)
opReq := c.Operation(b)
opReq.ConfigDir = cwd
opReq.ConfigLoader, err = c.initConfigLoader()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing config loader: %s", err))
return 1
}

// Get the context (required to get the schemas)
ctx, _, ctxDiags := local.Context(opReq)
if ctxDiags.HasErrors() {
c.showDiagnostics(ctxDiags)
return 1
}

// Get the schemas from the context
schemas := ctx.Schemas()

// Get the state
env := c.Workspace()
state, err := b.StateMgr(env)
stateMgr, err := b.StateMgr(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
if err := stateMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to refresh state: %s", err))
return 1
}

stateReal := state.State()
if stateReal == nil {
state := stateMgr.State()
if state == nil {
c.Ui.Error(fmt.Sprintf(errStateNotFound))
return 1
}

c.Ui.Error("state show not yet updated for new state types")
return 1

/*
filter := &terraform.StateFilter{State: stateReal}
results, err := filter.Filter(args...)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateFilter, err))
return 1
}

if len(results) == 0 {
return 0
}

instance, err := c.filterInstance(results)
if err != nil {
c.Ui.Error(err.Error())
return 1
}

if instance == nil {
return 0
}

is := instance.Value.(*terraform.InstanceState)

// Sort the keys
var keys []string
for k, _ := range is.Attributes {
keys = append(keys, k)
}
sort.Strings(keys)

// Build the output
var output []string
output = append(output, fmt.Sprintf("id | %s", is.ID))
for _, k := range keys {
if k != "id" {
output = append(output, fmt.Sprintf("%s | %s", k, is.Attributes[k]))
}
}

// Output
config := columnize.DefaultConfig()
config.Glue = " = "
c.Ui.Output(columnize.Format(output, config))
return 0
*/
is := state.ResourceInstance(addr)
if !is.HasCurrent() {
c.Ui.Error(errNoInstanceFound)
return 1
}

singleInstance := states.NewState()
singleInstance.EnsureModule(addr.Module).SetResourceInstanceCurrent(
addr.Resource,
is.Current,
addr.Resource.Resource.DefaultProviderConfig().Absolute(addr.Module),
)

output := format.State(&format.StateOpts{
State: singleInstance,
Color: c.Colorize(),
Schemas: schemas,
})
c.Ui.Output(output[strings.Index(output, "#"):])

return 0
}

func (c *StateShowCommand) Help() string {
Expand All @@ -125,3 +146,15 @@ Options:
func (c *StateShowCommand) Synopsis() string {
return "Show a resource in the state"
}

const errNoInstanceFound = `No instance found for the given address!

This command requires that the address references one specific instance.
To view the available instances, use "terraform state list". Please modify
the address to reference a specific instance.`

const errParsingAddress = `Error parsing instance address: %s

This command requires that the address references one specific instance.
To view the available instances, use "terraform state list". Please modify
the address to reference a specific instance.`
73 changes: 59 additions & 14 deletions command/state_show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"strings"
"testing"

"github.com/mitchellh/cli"

"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
)

func TestStateShow(t *testing.T) {
Expand All @@ -28,6 +30,18 @@ func TestStateShow(t *testing.T) {
statePath := testStateFile(t, state)

p := testProvider()
p.GetSchemaReturn = &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"foo": {Type: cty.String, Optional: true},
"bar": {Type: cty.String, Optional: true},
},
},
},
}

ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
Expand All @@ -45,14 +59,15 @@ func TestStateShow(t *testing.T) {
}

// Test that outputs were displayed
expected := strings.TrimSpace(testStateShowOutput) + "\n"
expected := strings.TrimSpace(testStateShowOutput) + "\n\n\n"
actual := ui.OutputWriter.String()
if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
}
}

func TestStateShow_multi(t *testing.T) {
submod, _ := addrs.ParseModuleInstanceStr("module.sub")
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Expand All @@ -70,18 +85,30 @@ func TestStateShow_multi(t *testing.T) {
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "bar",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
Name: "foo",
}.Instance(addrs.NoKey).Absolute(submod),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
Status: states.ObjectReady,
},
addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance),
addrs.ProviderConfig{Type: "test"}.Absolute(submod),
)
})
statePath := testStateFile(t, state)

p := testProvider()
p.GetSchemaReturn = &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"foo": {Type: cty.String, Optional: true},
"bar": {Type: cty.String, Optional: true},
},
},
},
}

ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
Expand All @@ -94,9 +121,16 @@ func TestStateShow_multi(t *testing.T) {
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code != 1 {
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}

// Test that outputs were displayed
expected := strings.TrimSpace(testStateShowOutput) + "\n\n\n"
actual := ui.OutputWriter.String()
if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
}
}

func TestStateShow_noState(t *testing.T) {
Expand All @@ -112,9 +146,14 @@ func TestStateShow_noState(t *testing.T) {
},
}

args := []string{}
args := []string{
"test_instance.foo",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
t.Fatalf("bad: %d", code)
}
if !strings.Contains(ui.ErrorWriter.String(), "No state file was found!") {
t.Fatalf("expected a no state file error, got: %s", ui.ErrorWriter.String())
}
}

Expand All @@ -135,13 +174,19 @@ func TestStateShow_emptyState(t *testing.T) {
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d", code)
}
if !strings.Contains(ui.ErrorWriter.String(), "No instance found for the given address!") {
t.Fatalf("expected a no instance found error, got: %s", ui.ErrorWriter.String())
}
}

const testStateShowOutput = `
id = bar
bar = value
foo = value
# test_instance.foo:
resource "test_instance" "foo" {
bar = "value"
foo = "value"
id = "bar"
}
`
2 changes: 1 addition & 1 deletion states/state_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (f *Filter) Filter(fs ...string) ([]*FilterResult, error) {
as[i] = addr
continue
}
return nil, fmt.Errorf("Error parsing address '%s'", v)
return nil, fmt.Errorf("Error parsing address: %s", v)
}

// If we weren't given any filters, then we list all
Expand Down