From f6692e66ac4d5ec73975727ec007bd87c79753e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 25 Mar 2016 10:17:25 -0700 Subject: [PATCH] add command/state show --- command/state_meta.go | 34 ++++++++++ command/state_show.go | 98 +++++++++++++++++++++++++++ command/state_show_test.go | 133 +++++++++++++++++++++++++++++++++++++ commands.go | 6 ++ terraform/state_filter.go | 13 +++- 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 command/state_meta.go create mode 100644 command/state_show.go create mode 100644 command/state_show_test.go diff --git a/command/state_meta.go b/command/state_meta.go new file mode 100644 index 000000000..f576004e3 --- /dev/null +++ b/command/state_meta.go @@ -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.` diff --git a/command/state_show.go b/command/state_show.go new file mode 100644 index 000000000..00c97816e --- /dev/null +++ b/command/state_show.go @@ -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" +} diff --git a/command/state_show_test.go b/command/state_show_test.go new file mode 100644 index 000000000..1e2dba08c --- /dev/null +++ b/command/state_show_test.go @@ -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 +` diff --git a/commands.go b/commands.go index 9ba629e4a..69454f2eb 100644 --- a/commands.go +++ b/commands.go @@ -158,6 +158,12 @@ func init() { Meta: meta, }, nil }, + + "state show": func() (cli.Command, error) { + return &command.StateShowCommand{ + Meta: meta, + }, nil + }, } } diff --git a/terraform/state_filter.go b/terraform/state_filter.go index 792fd2ab1..8b1f523c7 100644 --- a/terraform/state_filter.go +++ b/terraform/state_filter.go @@ -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`.