From 5458a91985e3dee49307e70913c622e4eb34ce39 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Fri, 26 Oct 2018 19:08:46 +0200 Subject: [PATCH] command/state: update and fix the state show command --- command/show.go | 1 - command/state_meta.go | 6 -- command/state_push.go | 4 +- command/state_show.go | 149 ++++++++++++++++++++++--------------- command/state_show_test.go | 73 ++++++++++++++---- states/state_filter.go | 2 +- 6 files changed, 153 insertions(+), 82 deletions(-) diff --git a/command/show.go b/command/show.go index 6d970f1bb..bb1106b59 100644 --- a/command/show.go +++ b/command/show.go @@ -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 diff --git a/command/state_meta.go b/command/state_meta.go index 247e66207..f823de880 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -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.` diff --git a/command/state_push.go b/command/state_push.go index ff7904914..9de232abc 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -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 diff --git a/command/state_show.go b/command/state_show.go index db59cbca1..8f501dcce 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -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" ) @@ -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) @@ -33,73 +42,85 @@ func (c *StateShowCommand) Run(args []string) int { return 1 } - // Get the state - env := c.Workspace() - state, err := b.StateMgr(env) - if err != nil { - 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)) + // We require a local backend + local, ok := b.(backend.Local) + if !ok { + c.Ui.Error(ErrUnsupportedLocalOp) return 1 } - stateReal := state.State() - if stateReal == nil { + // 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() + 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)) return 1 } - c.Ui.Error("state show not yet updated for new state types") - return 1 + is := state.ResourceInstance(addr) + if !is.HasCurrent() { + c.Ui.Error(errNoInstanceFound) + return 1 + } - /* - filter := &terraform.StateFilter{State: stateReal} - results, err := filter.Filter(args...) - if err != nil { - c.Ui.Error(fmt.Sprintf(errStateFilter, err)) - return 1 - } + singleInstance := states.NewState() + singleInstance.EnsureModule(addr.Module).SetResourceInstanceCurrent( + addr.Resource, + is.Current, + addr.Resource.Resource.DefaultProviderConfig().Absolute(addr.Module), + ) - if len(results) == 0 { - return 0 - } + output := format.State(&format.StateOpts{ + State: singleInstance, + Color: c.Colorize(), + Schemas: schemas, + }) + c.Ui.Output(output[strings.Index(output, "#"):]) - 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 - */ + return 0 } func (c *StateShowCommand) Help() string { @@ -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.` diff --git a/command/state_show_test.go b/command/state_show_test.go index c2f56c871..928a306f3 100644 --- a/command/state_show_test.go +++ b/command/state_show_test.go @@ -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) { @@ -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{ @@ -45,7 +59,7 @@ 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) @@ -53,6 +67,7 @@ func TestStateShow(t *testing.T) { } func TestStateShow_multi(t *testing.T) { + submod, _ := addrs.ParseModuleInstanceStr("module.sub") state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -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{ @@ -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) { @@ -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()) } } @@ -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" +} ` diff --git a/states/state_filter.go b/states/state_filter.go index 0f10a58dc..de5595cfa 100644 --- a/states/state_filter.go +++ b/states/state_filter.go @@ -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