diff --git a/command/format_plan.go b/command/format_plan.go index 6b7e69485..6ebf2ecc8 100644 --- a/command/format_plan.go +++ b/command/format_plan.go @@ -12,6 +12,10 @@ import ( // FormatPlan takes a plan and returns a func FormatPlan(p *terraform.Plan, c *colorstring.Colorize) string { + if p.Diff == nil || p.Diff.Empty() { + return "This plan does nothing." + } + if c == nil { c = &colorstring.Colorize{ Colors: colorstring.DefaultColors, diff --git a/command/format_state.go b/command/format_state.go new file mode 100644 index 000000000..2b31a9b51 --- /dev/null +++ b/command/format_state.go @@ -0,0 +1,85 @@ +package command + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/colorstring" +) + +// FormatState takes a state and returns a string +func FormatState(s *terraform.State, c *colorstring.Colorize) string { + if len(s.Resources) == 0 { + return "The state file is empty. No resources are represented." + } + + if c == nil { + c = &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Reset: false, + } + } + + buf := new(bytes.Buffer) + buf.WriteString("[reset]") + + // First get the names of all the resources so we can show them + // in alphabetical order. + names := make([]string, 0, len(s.Resources)) + for name, _ := range s.Resources { + names = append(names, name) + } + sort.Strings(names) + + // Go through each resource and begin building up the output. + for _, k := range names { + rs := s.Resources[k] + id := rs.ID + if id == "" { + id = "" + } + + buf.WriteString(fmt.Sprintf("%s:\n", k)) + buf.WriteString(fmt.Sprintf(" id = %s\n", id)) + + // Sort the attributes + attrKeys := make([]string, 0, len(rs.Attributes)) + for ak, _ := range rs.Attributes { + // Skip the id attribute since we just show the id directly + if ak == "id" { + continue + } + + attrKeys = append(attrKeys, ak) + } + sort.Strings(attrKeys) + + // Output each attribute + for _, ak := range attrKeys { + av := rs.Attributes[ak] + buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) + } + } + + if len(s.Outputs) > 0 { + buf.WriteString("\nOutputs:\n\n") + + // Sort the outputs + ks := make([]string, 0, len(s.Outputs)) + for k, _ := range s.Outputs { + ks = append(ks, k) + } + sort.Strings(ks) + + // Output each output k/v pair + for _, k := range ks { + v := s.Outputs[k] + buf.WriteString(fmt.Sprintf("%s = %s\n", k, v)) + } + } + + return strings.TrimSpace(buf.String()) +} diff --git a/command/graph_test.go b/command/graph_test.go index f95089def..0a74726e7 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -32,7 +32,7 @@ func TestGraph(t *testing.T) { func TestGraph_multipleArgs(t *testing.T) { ui := new(cli.MockUi) - c := &ApplyCommand{ + c := &GraphCommand{ ContextOpts: testCtxConfig(testProvider()), Ui: ui, } diff --git a/command/show.go b/command/show.go new file mode 100644 index 000000000..0d5ebce59 --- /dev/null +++ b/command/show.go @@ -0,0 +1,96 @@ +package command + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +// ShowCommand is a Command implementation that reads and outputs the +// contents of a Terraform plan or state file. +type ShowCommand struct { + ContextOpts *terraform.ContextOpts + Ui cli.Ui +} + +func (c *ShowCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("show", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + args = cmdFlags.Args() + if len(args) != 1 { + c.Ui.Error( + "The show command expects exactly one argument with the path\n" + + "to a Terraform state or plan file.\n") + cmdFlags.Usage() + return 1 + } + path := args[0] + + var plan *terraform.Plan + var state *terraform.State + + f, err := os.Open(path) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error loading file: %s", err)) + return 1 + } + + var planErr, stateErr error + plan, err = terraform.ReadPlan(f) + if err != nil { + if _, err := f.Seek(0, 0); err != nil { + c.Ui.Error(fmt.Sprintf("Error reading file: %s", err)) + return 1 + } + + plan = nil + planErr = err + } + if plan == nil { + state, err = terraform.ReadState(f) + if err != nil { + stateErr = err + } + } + if plan == nil && state == nil { + c.Ui.Error(fmt.Sprintf( + "Terraform couldn't read the given file as a state or plan file.\n"+ + "The errors while attempting to read the file as each format are\n"+ + "shown below.\n\n"+ + "State read error: %s\n\nPlan read error: %s", + stateErr, + planErr)) + return 1 + } + + if plan != nil { + c.Ui.Output(FormatPlan(plan, nil)) + return 0 + } + + c.Ui.Output(FormatState(state, nil)) + return 0 +} + +func (c *ShowCommand) Help() string { + helpText := ` +Usage: terraform show [options] path + + Reads and outputs a Terraform state or plan file in a human-readable + form. + +` + return strings.TrimSpace(helpText) +} + +func (c *ShowCommand) Synopsis() string { + return "Inspect Terraform state or plan" +} diff --git a/command/show_test.go b/command/show_test.go new file mode 100644 index 000000000..b33f630e9 --- /dev/null +++ b/command/show_test.go @@ -0,0 +1,83 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestShow(t *testing.T) { + ui := new(cli.MockUi) + c := &ShowCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + "bad", + "bad", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + } +} + +func TestShow_noArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &ShowCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + } +} + +func TestShow_plan(t *testing.T) { + planPath := testPlanFile(t, &terraform.Plan{ + Config: new(config.Config), + }) + + ui := new(cli.MockUi) + c := &ShowCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + planPath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestShow_state(t *testing.T) { + originalState := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &ShowCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + statePath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} diff --git a/commands.go b/commands.go index 48d57cbfe..d7aa46eb9 100644 --- a/commands.go +++ b/commands.go @@ -56,6 +56,13 @@ func init() { }, nil }, + "show": func() (cli.Command, error) { + return &command.ShowCommand{ + ContextOpts: &ContextOpts, + Ui: Ui, + }, nil + }, + "version": func() (cli.Command, error) { return &command.VersionCommand{ Revision: GitCommit,