command/state: update and fix the state show command

This commit is contained in:
Sander van Harmelen 2018-10-26 19:08:46 +02:00
parent 292ec47e66
commit 5458a91985
6 changed files with 153 additions and 82 deletions

View File

@ -23,7 +23,6 @@ type ShowCommand struct {
} }
func (c *ShowCommand) Run(args []string) int { func (c *ShowCommand) Run(args []string) int {
args, err := c.Meta.process(args, false) args, err := c.Meta.process(args, false)
if err != nil { if err != nil {
return 1 return 1

View File

@ -133,9 +133,3 @@ func (c *StateMeta) filter(state *states.State, args []string) ([]*states.Filter
return results, nil 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.`

View File

@ -32,8 +32,8 @@ func (c *StatePushCommand) Run(args []string) int {
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) != 1 { if len(args) != 1 {
c.Ui.Error("Exactly one argument expected: path to state to push") c.Ui.Error("Exactly one argument expected.\n")
return 1 return cli.RunResultHelp
} }
// Determine our reader for the input state. This is the filepath // Determine our reader for the input state. This is the filepath

View File

@ -2,8 +2,13 @@ package command
import ( import (
"fmt" "fmt"
"os"
"strings" "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" "github.com/mitchellh/cli"
) )
@ -20,11 +25,15 @@ func (c *StateShowCommand) Run(args []string) int {
} }
cmdFlags := c.Meta.flagSet("state show") 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 { if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp return cli.RunResultHelp
} }
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) != 1 {
c.Ui.Error("Exactly one argument expected.\n")
return cli.RunResultHelp
}
// Load the backend // Load the backend
b, backendDiags := c.Backend(nil) b, backendDiags := c.Backend(nil)
@ -33,73 +42,85 @@ func (c *StateShowCommand) Run(args []string) int {
return 1 return 1
} }
// Get the state // We require a local backend
env := c.Workspace() local, ok := b.(backend.Local)
state, err := b.StateMgr(env) if !ok {
if err != nil { c.Ui.Error(ErrUnsupportedLocalOp)
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
stateReal := state.State() // Check if the address can be parsed
if stateReal == nil { 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()
stateMgr, err := b.StateMgr(env)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return 1
}
if err := stateMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to refresh state: %s", err))
return 1
}
state := stateMgr.State()
if state == nil {
c.Ui.Error(fmt.Sprintf(errStateNotFound)) c.Ui.Error(fmt.Sprintf(errStateNotFound))
return 1 return 1
} }
c.Ui.Error("state show not yet updated for new state types") is := state.ResourceInstance(addr)
return 1 if !is.HasCurrent() {
c.Ui.Error(errNoInstanceFound)
return 1
}
/* singleInstance := states.NewState()
filter := &terraform.StateFilter{State: stateReal} singleInstance.EnsureModule(addr.Module).SetResourceInstanceCurrent(
results, err := filter.Filter(args...) addr.Resource,
if err != nil { is.Current,
c.Ui.Error(fmt.Sprintf(errStateFilter, err)) addr.Resource.Resource.DefaultProviderConfig().Absolute(addr.Module),
return 1 )
}
if len(results) == 0 { output := format.State(&format.StateOpts{
return 0 State: singleInstance,
} Color: c.Colorize(),
Schemas: schemas,
})
c.Ui.Output(output[strings.Index(output, "#"):])
instance, err := c.filterInstance(results) return 0
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
*/
} }
func (c *StateShowCommand) Help() string { func (c *StateShowCommand) Help() string {
@ -125,3 +146,15 @@ Options:
func (c *StateShowCommand) Synopsis() string { func (c *StateShowCommand) Synopsis() string {
return "Show a resource in the state" 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.`

View File

@ -4,10 +4,12 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/states" "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) { func TestStateShow(t *testing.T) {
@ -28,6 +30,18 @@ func TestStateShow(t *testing.T) {
statePath := testStateFile(t, state) statePath := testStateFile(t, state)
p := testProvider() 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) ui := new(cli.MockUi)
c := &StateShowCommand{ c := &StateShowCommand{
Meta: Meta{ Meta: Meta{
@ -45,7 +59,7 @@ func TestStateShow(t *testing.T) {
} }
// Test that outputs were displayed // Test that outputs were displayed
expected := strings.TrimSpace(testStateShowOutput) + "\n" expected := strings.TrimSpace(testStateShowOutput) + "\n\n\n"
actual := ui.OutputWriter.String() actual := ui.OutputWriter.String()
if actual != expected { if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected) t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
@ -53,6 +67,7 @@ func TestStateShow(t *testing.T) {
} }
func TestStateShow_multi(t *testing.T) { func TestStateShow_multi(t *testing.T) {
submod, _ := addrs.ParseModuleInstanceStr("module.sub")
state := states.BuildState(func(s *states.SyncState) { state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent( s.SetResourceInstanceCurrent(
addrs.Resource{ addrs.Resource{
@ -70,18 +85,30 @@ func TestStateShow_multi(t *testing.T) {
addrs.Resource{ addrs.Resource{
Mode: addrs.ManagedResourceMode, Mode: addrs.ManagedResourceMode,
Type: "test_instance", Type: "test_instance",
Name: "bar", Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), }.Instance(addrs.NoKey).Absolute(submod),
&states.ResourceInstanceObjectSrc{ &states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
Status: states.ObjectReady, Status: states.ObjectReady,
}, },
addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), addrs.ProviderConfig{Type: "test"}.Absolute(submod),
) )
}) })
statePath := testStateFile(t, state) statePath := testStateFile(t, state)
p := testProvider() 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) ui := new(cli.MockUi)
c := &StateShowCommand{ c := &StateShowCommand{
Meta: Meta{ Meta: Meta{
@ -94,9 +121,16 @@ func TestStateShow_multi(t *testing.T) {
"-state", statePath, "-state", statePath,
"test_instance.foo", "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()) 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) { func TestStateShow_noState(t *testing.T) {
@ -112,9 +146,14 @@ func TestStateShow_noState(t *testing.T) {
}, },
} }
args := []string{} args := []string{
"test_instance.foo",
}
if code := c.Run(args); code != 1 { 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())
} }
} }
@ -135,13 +174,19 @@ func TestStateShow_emptyState(t *testing.T) {
"-state", statePath, "-state", statePath,
"test_instance.foo", "test_instance.foo",
} }
if code := c.Run(args); code != 0 { 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 instance found for the given address!") {
t.Fatalf("expected a no instance found error, got: %s", ui.ErrorWriter.String())
} }
} }
const testStateShowOutput = ` const testStateShowOutput = `
id = bar # test_instance.foo:
bar = value resource "test_instance" "foo" {
foo = value bar = "value"
foo = "value"
id = "bar"
}
` `

View File

@ -40,7 +40,7 @@ func (f *Filter) Filter(fs ...string) ([]*FilterResult, error) {
as[i] = addr as[i] = addr
continue 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 // If we weren't given any filters, then we list all