add command/state show

This commit is contained in:
Mitchell Hashimoto 2016-03-25 10:17:25 -07:00 committed by James Nugent
parent a754723561
commit f6692e66ac
5 changed files with 282 additions and 2 deletions

34
command/state_meta.go Normal file
View File

@ -0,0 +1,34 @@
package command
import (
"errors"
"github.com/hashicorp/terraform/terraform"
)
// StateMeta is the meta struct that should be embedded in state subcommands.
type StateMeta struct{}
// filterInstance filters a single instance out of filter results.
func (c *StateMeta) filterInstance(rs []*terraform.StateFilterResult) (*terraform.StateFilterResult, error) {
var result *terraform.StateFilterResult
for _, r := range rs {
if _, ok := r.Value.(*terraform.InstanceState); !ok {
continue
}
if result != nil {
return nil, errors.New(errStateMultiple)
}
result = r
}
return result, 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.`

98
command/state_show.go Normal file
View File

@ -0,0 +1,98 @@
package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/ryanuber/columnize"
)
// StateShowCommand is a Command implementation that shows a single resource.
type StateShowCommand struct {
Meta
StateMeta
}
func (c *StateShowCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("state show")
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
if err := cmdFlags.Parse(args); err != nil {
return cli.RunResultHelp
}
args = cmdFlags.Args()
state, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))
return cli.RunResultHelp
}
stateReal := state.State()
if stateReal == nil {
c.Ui.Error(fmt.Sprintf(errStateNotFound))
return 1
}
filter := &terraform.StateFilter{State: stateReal}
results, err := filter.Filter(args...)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateFilter, err))
return 1
}
instance, err := c.filterInstance(results)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
is := instance.Value.(*terraform.InstanceState)
// Sort the keys
keys := make([]string, 0, len(is.Attributes))
for k, _ := range is.Attributes {
keys = append(keys, k)
}
sort.Strings(keys)
// Build the output
output := make([]string, 0, len(is.Attributes)+1)
output = append(output, fmt.Sprintf("id | %s", is.ID))
for _, k := range keys {
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 {
helpText := `
Usage: terraform state show [options] PATTERN
Shows the attributes of a resource in the Terraform state.
This command shows the attributes of a single resource in the Terraform
state. The pattern argument must be used to specify a single resource.
You can view the list of available resources with "terraform state list".
Options:
-state=statefile Path to a Terraform state file to use to look
up Terraform-managed resources. By default it will
use the state "terraform.tfstate" if it exists.
`
return strings.TrimSpace(helpText)
}
func (c *StateShowCommand) Synopsis() string {
return "Show a resource in the state"
}

133
command/state_show_test.go Normal file
View File

@ -0,0 +1,133 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
func TestStateShow(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
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"
actual := ui.OutputWriter.String()
if actual != expected {
t.Fatalf("Expected:\n%q\n\nTo equal: %q", actual, expected)
}
}
func TestStateShow_multi(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo.0": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
"test_instance.foo.1": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"foo": "value",
"bar": "value",
},
},
},
},
},
},
}
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestStateShow_noState(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
const testStateShowOutput = `
id = bar
bar = value
foo = value
`

View File

@ -158,6 +158,12 @@ func init() {
Meta: meta,
}, nil
},
"state show": func() (cli.Command, error) {
return &command.StateShowCommand{
Meta: meta,
}, nil
},
}
}

View File

@ -108,11 +108,12 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
}
// Add the resource level result
results = append(results, &StateFilterResult{
resourceResult := &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Value: r,
})
}
results = append(results, resourceResult)
// Add the instances
if r.Primary != nil {
@ -120,6 +121,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: r.Primary,
})
}
@ -130,6 +132,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
@ -141,6 +144,7 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult {
results = append(results, &StateFilterResult{
Path: addr.Path,
Address: addr.String(),
Parent: resourceResult,
Value: instance,
})
}
@ -200,6 +204,11 @@ type StateFilterResult struct {
// Address is the address that can be used to reference this exact result.
Address string
// Parent, if non-nil, is a parent of this result. For instances, the
// parent would be a resource. For resources, the parent would be
// a module. For modules, this is currently nil.
Parent *StateFilterResult
// Value is the actual value. This must be type switched on. It can be
// any data structures that `State` can hold: `ModuleState`,
// `ResourceState`, `InstanceState`.