From 8f7244695f49311c38673a8bfe246ca3698963fb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 20:38:03 -0700 Subject: [PATCH 01/36] command/graph: takes config dir as arg --- command/command_test.go | 2 +- command/graph.go | 15 ++++++-- command/graph_test.go | 57 +++++++++++++++++++++++++++++ command/test-fixtures/graph/main.tf | 3 ++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 command/graph_test.go create mode 100644 command/test-fixtures/graph/main.tf diff --git a/command/command_test.go b/command/command_test.go index 2f3207aa7..e2165bafa 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -13,7 +13,7 @@ import ( const fixtureDir = "./test-fixtures" func testFixturePath(name string) string { - return filepath.Join(fixtureDir, name, "main.tf") + return filepath.Join(fixtureDir, name) } func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts { diff --git a/command/graph.go b/command/graph.go index 0d36166cd..02ff969de 100644 --- a/command/graph.go +++ b/command/graph.go @@ -1,9 +1,9 @@ package command import ( + "bytes" "flag" "fmt" - "os" "strings" "github.com/hashicorp/terraform/config" @@ -33,7 +33,9 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - conf, err := config.Load(args[0]) + path := args[0] + + conf, err := config.LoadDir(path) if err != nil { c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) return 1 @@ -48,11 +50,14 @@ func (c *GraphCommand) Run(args []string) int { return 1 } + buf := new(bytes.Buffer) nodes := make([]digraph.Node, len(g.Nouns)) for i, n := range g.Nouns { nodes[i] = n } - digraph.GenerateDot(nodes, os.Stdout) + digraph.GenerateDot(nodes, buf) + + c.Ui.Output(buf.String()) return 0 } @@ -66,6 +71,10 @@ Usage: terraform graph [options] PATH shown. If the path is a plan file, then the dependency graph of the plan itself is shown. + The graph is outputted in DOT format. The typical program that can + read this format is GraphViz, but many web services are also available + to read this format. + ` return strings.TrimSpace(helpText) } diff --git a/command/graph_test.go b/command/graph_test.go new file mode 100644 index 000000000..382292340 --- /dev/null +++ b/command/graph_test.go @@ -0,0 +1,57 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestGraph(t *testing.T) { + ui := new(cli.MockUi) + c := &GraphCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + testFixturePath("graph"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "digraph {") { + t.Fatalf("doesn't look like digraph: %s", output) + } +} + +func TestGraph_noArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &ApplyCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + } +} + +func TestGraph_multipleArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &ApplyCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + "bad", + "bad", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + } +} diff --git a/command/test-fixtures/graph/main.tf b/command/test-fixtures/graph/main.tf new file mode 100644 index 000000000..5794f94d9 --- /dev/null +++ b/command/test-fixtures/graph/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "foo" { + ami = "bar" +} From 235a253848e93081b1799fec7255f2bf51bbe6a1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 20:41:47 -0700 Subject: [PATCH 02/36] command/graph: no args means pwd --- command/graph.go | 14 +++++++++++--- command/graph_test.go | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/command/graph.go b/command/graph.go index 02ff969de..aec4ffecd 100644 --- a/command/graph.go +++ b/command/graph.go @@ -4,6 +4,7 @@ import ( "bytes" "flag" "fmt" + "os" "strings" "github.com/hashicorp/terraform/config" @@ -26,15 +27,22 @@ func (c *GraphCommand) Run(args []string) int { return 1 } + var path string args = cmdFlags.Args() - if len(args) != 1 { + if len(args) > 1 { c.Ui.Error("The graph command expects one argument.\n") cmdFlags.Usage() return 1 + } else if len(args) == 1 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + } } - path := args[0] - conf, err := config.LoadDir(path) if err != nil { c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) diff --git a/command/graph_test.go b/command/graph_test.go index 382292340..1a1e298f9 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -1,6 +1,7 @@ package command import ( + "os" "strings" "testing" @@ -27,19 +28,6 @@ func TestGraph(t *testing.T) { } } -func TestGraph_noArgs(t *testing.T) { - ui := new(cli.MockUi) - c := &ApplyCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, - } - - args := []string{} - if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) - } -} - func TestGraph_multipleArgs(t *testing.T) { ui := new(cli.MockUi) c := &ApplyCommand{ @@ -55,3 +43,30 @@ func TestGraph_multipleArgs(t *testing.T) { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } } + +func TestGraph_noArgs(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("graph")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &GraphCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "digraph {") { + t.Fatalf("doesn't look like digraph: %s", output) + } +} From 04f7281e8c23291347efb519f8032637cee08e83 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 20:51:26 -0700 Subject: [PATCH 03/36] command/plan: can take no args and default to pwd for config --- command/plan.go | 28 +++++++++++++++++++++------- command/plan_test.go | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/command/plan.go b/command/plan.go index 06cb9e7bb..e41466087 100644 --- a/command/plan.go +++ b/command/plan.go @@ -33,13 +33,22 @@ func (c *PlanCommand) Run(args []string) int { return 1 } + var path string args = cmdFlags.Args() - if len(args) != 1 { + if len(args) > 1 { c.Ui.Error( - "The plan command expects only one argument with the path\n" + + "The plan command expects at most one argument with the path\n" + "to a Terraform configuration.\n") cmdFlags.Usage() return 1 + } else if len(args) == 1 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + } } // Load up the state @@ -59,7 +68,7 @@ func (c *PlanCommand) Run(args []string) int { } } - b, err := config.Load(args[0]) + b, err := config.LoadDir(path) if err != nil { c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err)) return 1 @@ -111,10 +120,14 @@ func (c *PlanCommand) Run(args []string) int { func (c *PlanCommand) Help() string { helpText := ` -Usage: terraform plan [options] [terraform.tf] +Usage: terraform plan [options] [dir] - Shows the differences between the Terraform configuration and - the actual state of an infrastructure. + Generates an execution plan for Terraform. + + This execution plan can be reviewed prior to running apply to get a + sense for what Terraform will do. Optionally, the plan can be saved to + a Terraform plan file, and apply can take this plan file to execute + this plan exactly. Options: @@ -127,7 +140,8 @@ Options: -refresh=true Update state prior to checking for differences. -state=statefile Path to a Terraform state file to use to look - up Terraform-managed resources. + up Terraform-managed resources. By default it will + use the state "terraform.tfstate" if it exists. ` return strings.TrimSpace(helpText) diff --git a/command/plan_test.go b/command/plan_test.go index 84963d318..61c22b8a6 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -10,6 +10,29 @@ import ( "github.com/mitchellh/cli" ) +func TestPlan(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("plan")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + func TestPlan_destroy(t *testing.T) { originalState := &terraform.State{ Resources: map[string]*terraform.ResourceState{ From 6bf543cb07e1950dcfc5508bc2ea15a1c1b30b72 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:03:56 -0700 Subject: [PATCH 04/36] command/plan: default state path --- command/command.go | 3 +++ command/command_test.go | 12 ++++++++- command/plan.go | 13 +++++++-- command/plan_test.go | 60 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/command/command.go b/command/command.go index fd316f4b1..6ca862d83 100644 --- a/command/command.go +++ b/command/command.go @@ -9,6 +9,9 @@ import ( "github.com/mitchellh/cli" ) +// DefaultStateFilename is the default filename used for the state file. +const DefaultStateFilename = "terraform.tfstate" + func ContextArg( path string, statePath string, diff --git a/command/command_test.go b/command/command_test.go index e2165bafa..0f231ba90 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -10,7 +10,17 @@ import ( ) // This is the directory where our test fixtures are. -const fixtureDir = "./test-fixtures" +var fixtureDir = "./test-fixtures" + +func init() { + // Expand the fixture dir on init because we change the working + // directory in some tests. + var err error + fixtureDir, err = filepath.Abs(fixtureDir) + if err != nil { + panic(err) + } +} func testFixturePath(name string) string { return filepath.Join(fixtureDir, name) diff --git a/command/plan.go b/command/plan.go index e41466087..d1c24482f 100644 --- a/command/plan.go +++ b/command/plan.go @@ -27,7 +27,7 @@ func (c *PlanCommand) Run(args []string) int { cmdFlags.BoolVar(&destroy, "destroy", false, "destroy") cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&outPath, "out", "", "path") - cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 @@ -51,6 +51,15 @@ func (c *PlanCommand) Run(args []string) int { } } + // If the default state path doesn't exist, ignore it. + if statePath != "" { + if _, err := os.Stat(statePath); err != nil { + if os.IsNotExist(err) && statePath == DefaultStateFilename { + statePath = "" + } + } + } + // Load up the state var state *terraform.State if statePath != "" { @@ -70,7 +79,7 @@ func (c *PlanCommand) Run(args []string) int { b, err := config.LoadDir(path) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err)) + c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) return 1 } diff --git a/command/plan_test.go b/command/plan_test.go index 61c22b8a6..f31c82356 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -3,6 +3,7 @@ package command import ( "io/ioutil" "os" + "path/filepath" "reflect" "testing" @@ -207,3 +208,62 @@ func TestPlan_state(t *testing.T) { t.Fatalf("bad: %#v", p.DiffState) } } + +func TestPlan_stateDefault(t *testing.T) { + originalState := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + + // Write the state file in a temporary directory with the + // default filename. + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + statePath := filepath.Join(td, DefaultStateFilename) + + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(originalState, f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Change to that directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + args := []string{ + testFixturePath("plan"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Verify that the provider was called with the existing state + expectedState := originalState.Resources["test_instance.foo"] + if !reflect.DeepEqual(p.DiffState, expectedState) { + t.Fatalf("bad: %#v", p.DiffState) + } +} From 7a01e781ab52407d89950c1ca39fce940693193d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:04:59 -0700 Subject: [PATCH 05/36] config: LoadDir with no matching files errors --- config/loader.go | 7 +++++++ config/loader_test.go | 7 +++++++ config/test-fixtures/dir-empty/.gitkeep | 0 3 files changed, 14 insertions(+) create mode 100644 config/test-fixtures/dir-empty/.gitkeep diff --git a/config/loader.go b/config/loader.go index 750e9d15d..a8cbb5adf 100644 --- a/config/loader.go +++ b/config/loader.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "path/filepath" ) @@ -35,6 +36,12 @@ func LoadDir(path string) (*Config, error) { return nil, err } + if len(matches) == 0 { + return nil, fmt.Errorf( + "No Terraform configuration files found in directory: %s", + path) + } + var result *Config for _, f := range matches { c, err := Load(f) diff --git a/config/loader_test.go b/config/loader_test.go index 2737ab38b..4181dcb34 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -128,6 +128,13 @@ func TestLoadDir_basic(t *testing.T) { } } +func TestLoadDir_noConfigs(t *testing.T) { + _, err := LoadDir(filepath.Join(fixtureDir, "dir-empty")) + if err == nil { + t.Fatal("should error") + } +} + func outputsStr(os map[string]*Output) string { ns := make([]string, 0, len(os)) for n, _ := range os { diff --git a/config/test-fixtures/dir-empty/.gitkeep b/config/test-fixtures/dir-empty/.gitkeep new file mode 100644 index 000000000..e69de29bb From abc6df2a7df92c5fff90c6f19d36598732635835 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:30:40 -0700 Subject: [PATCH 06/36] command/apply: default state path, only one optional arg --- command/apply.go | 39 ++++++++++++++++---------- command/apply_test.go | 65 +++++++++++++++++++++++++++++++++++++++---- command/command.go | 2 +- 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/command/apply.go b/command/apply.go index 448258dec..13b1b7167 100644 --- a/command/apply.go +++ b/command/apply.go @@ -20,36 +20,39 @@ type ApplyCommand struct { func (c *ApplyCommand) Run(args []string) int { var init bool - var stateOutPath string + var statePath, stateOutPath string cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError) cmdFlags.BoolVar(&init, "init", false, "init") - cmdFlags.StringVar(&stateOutPath, "out", "", "path") + cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } args = cmdFlags.Args() - if len(args) != 2 { - c.Ui.Error("The apply command expects two arguments.\n") + if len(args) > 1 { + c.Ui.Error("The apply command expacts at most one argument.") cmdFlags.Usage() return 1 } + configPath := args[0] - statePath := args[0] - configPath := args[1] - + // If we don't specify an output path, default to out normal state + // path. if stateOutPath == "" { stateOutPath = statePath } + // The state path to use to generate a plan. If we're initializing + // a new infrastructure, then we don't use a state path. planStatePath := statePath if init { planStatePath = "" } - // Initialize Terraform right away + // Build the context based on the arguments given c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) ctx, err := ContextArg(configPath, planStatePath, c.ContextOpts) if err != nil { @@ -113,18 +116,24 @@ func (c *ApplyCommand) Run(args []string) int { func (c *ApplyCommand) Help() string { helpText := ` -Usage: terraform apply [options] STATE PATH +Usage: terraform apply [options] [dir] - Builds or changes infrastructure according to the Terraform configuration - file. + Builds or changes infrastructure according to Terraform configuration + files . Options: - -init If specified, it is okay to build brand new - infrastructure (with no state file specified). + -init If specified, new infrastructure can be built (no + previous state). This is just a safety switch + to prevent accidentally spinning up a new + infrastructure. - -out=file.tfstate Path to save the new state. If not specified, the - state path argument will be used. + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". + + -state-out=path Path to write state to that is different than + "-state". This can be used to preserve the old + state. ` return strings.TrimSpace(helpText) diff --git a/command/apply_test.go b/command/apply_test.go index 750e533f5..b068e7600 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -2,7 +2,9 @@ package command import ( "fmt" + "io/ioutil" "os" + "path/filepath" "reflect" "sync" "testing" @@ -25,7 +27,7 @@ func TestApply(t *testing.T) { args := []string{ "-init", - statePath, + "-state", statePath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { @@ -61,7 +63,7 @@ func TestApply_configInvalid(t *testing.T) { args := []string{ "-init", - testTempFile(t), + "-state", testTempFile(t), testFixturePath("apply-config-invalid"), } if code := c.Run(args); code != 1 { @@ -69,6 +71,57 @@ func TestApply_configInvalid(t *testing.T) { } } +func TestApply_defaultState(t *testing.T) { + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + statePath := filepath.Join(td, DefaultStateFilename) + + // Change to the temporary directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + args := []string{ + "-init", + testFixturePath("apply"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + state, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + if state == nil { + t.Fatal("state should not be nil") + } +} + func TestApply_error(t *testing.T) { statePath := testTempFile(t) @@ -108,7 +161,7 @@ func TestApply_error(t *testing.T) { args := []string{ "-init", - statePath, + "-state", statePath, testFixturePath("apply-error"), } if code := c.Run(args); code != 1 { @@ -151,7 +204,7 @@ func TestApply_plan(t *testing.T) { } args := []string{ - statePath, + "-state", statePath, planPath, } if code := c.Run(args); code != 0 { @@ -235,7 +288,7 @@ func TestApply_shutdown(t *testing.T) { args := []string{ "-init", - statePath, + "-state", statePath, testFixturePath("apply-shutdown"), } if code := c.Run(args); code != 0 { @@ -294,7 +347,7 @@ func TestApply_state(t *testing.T) { // Run the apply command pointing to our existing state args := []string{ - statePath, + "-state", statePath, testFixturePath("apply"), } if code := c.Run(args); code != 0 { diff --git a/command/command.go b/command/command.go index 6ca862d83..b8f4aab9d 100644 --- a/command/command.go +++ b/command/command.go @@ -55,7 +55,7 @@ func ContextArg( } } - config, err := config.Load(path) + config, err := config.LoadDir(path) if err != nil { return nil, fmt.Errorf("Error loading config: %s", err) } From 2c77837a640e66ca6432eedff014f303ba399b86 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:32:34 -0700 Subject: [PATCH 07/36] command/apply: optional arg, default to pwd for config dir --- command/apply.go | 10 +++++++++- command/apply_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/command/apply.go b/command/apply.go index 13b1b7167..636a8daa7 100644 --- a/command/apply.go +++ b/command/apply.go @@ -31,13 +31,21 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } + var configPath string args = cmdFlags.Args() if len(args) > 1 { c.Ui.Error("The apply command expacts at most one argument.") cmdFlags.Usage() return 1 + } else if len(args) == 1 { + configPath = args[0] + } else { + var err error + configPath, err = os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + } } - configPath := args[0] // If we don't specify an output path, default to out normal state // path. diff --git a/command/apply_test.go b/command/apply_test.go index b068e7600..58a831a2f 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -190,6 +190,52 @@ func TestApply_error(t *testing.T) { } } +func TestApply_noArgs(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("plan")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + statePath := testTempFile(t) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + args := []string{ + "-init", + "-state", statePath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + state, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + if state == nil { + t.Fatal("state should not be nil") + } +} + func TestApply_plan(t *testing.T) { planPath := testPlanFile(t, &terraform.Plan{ Config: new(config.Config), From 539a6c244eefe24d4d66f2c9b2bd404dc8cd7732 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:42:09 -0700 Subject: [PATCH 08/36] terraform: Read/WriteState sets magic bytes --- terraform/state.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/terraform/state.go b/terraform/state.go index 49dca37f1..8fef99889 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -149,15 +149,31 @@ func (s *sensitiveState) init() { // The format byte is prefixed into the state file format so that we have // the ability in the future to change the file format if we want for any // reason. -const stateFormatByte byte = 1 +const stateFormatMagic = "tfstate" +const stateFormatVersion byte = 1 // ReadState reads a state structure out of a reader in the format that // was written by WriteState. func ReadState(src io.Reader) (*State, error) { var result *State + var err error + n := 0 + // Verify the magic bytes + magic := make([]byte, len(stateFormatMagic)) + for n < len(magic) { + n, err = src.Read(magic[n:]) + if err != nil { + return nil, fmt.Errorf("error while reading magic bytes: %s", err) + } + } + if string(magic) != stateFormatMagic { + return nil, fmt.Errorf("not a valid state file") + } + + // Verify the version is something we can read var formatByte [1]byte - n, err := src.Read(formatByte[:]) + n, err = src.Read(formatByte[:]) if err != nil { return nil, err } @@ -165,10 +181,11 @@ func ReadState(src io.Reader) (*State, error) { return nil, errors.New("failed to read state version byte") } - if formatByte[0] != stateFormatByte { + if formatByte[0] != stateFormatVersion { return nil, fmt.Errorf("unknown state file version: %d", formatByte[0]) } + // Decode dec := gob.NewDecoder(src) if err := dec.Decode(&result); err != nil { return nil, err @@ -179,7 +196,17 @@ func ReadState(src io.Reader) (*State, error) { // WriteState writes a state somewhere in a binary format. func WriteState(d *State, dst io.Writer) error { - n, err := dst.Write([]byte{stateFormatByte}) + // Write the magic bytes so we can determine the file format later + n, err := dst.Write([]byte(stateFormatMagic)) + if err != nil { + return err + } + if n != len(stateFormatMagic) { + return errors.New("failed to write state format magic bytes") + } + + // Write a version byte so we can iterate on version at some point + n, err = dst.Write([]byte{stateFormatVersion}) if err != nil { return err } From 1911ee215bb629b549ba490d23618ddfb57b915d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:43:24 -0700 Subject: [PATCH 09/36] terraform: plan read/write sets magic bytes so we can test format --- terraform/plan.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/terraform/plan.go b/terraform/plan.go index 081fc6985..739561f1f 100644 --- a/terraform/plan.go +++ b/terraform/plan.go @@ -82,15 +82,31 @@ func (p *Plan) init() { // The format byte is prefixed into the plan file format so that we have // the ability in the future to change the file format if we want for any // reason. -const planFormatByte byte = 1 +const planFormatMagic = "tfplan" +const planFormatVersion byte = 1 // ReadPlan reads a plan structure out of a reader in the format that // was written by WritePlan. func ReadPlan(src io.Reader) (*Plan, error) { var result *Plan + var err error + n := 0 + // Verify the magic bytes + magic := make([]byte, len(planFormatMagic)) + for n < len(magic) { + n, err = src.Read(magic[n:]) + if err != nil { + return nil, fmt.Errorf("error while reading magic bytes: %s", err) + } + } + if string(magic) != planFormatMagic { + return nil, fmt.Errorf("not a valid plan file") + } + + // Verify the version is something we can read var formatByte [1]byte - n, err := src.Read(formatByte[:]) + n, err = src.Read(formatByte[:]) if err != nil { return nil, err } @@ -98,7 +114,7 @@ func ReadPlan(src io.Reader) (*Plan, error) { return nil, errors.New("failed to read plan version byte") } - if formatByte[0] != planFormatByte { + if formatByte[0] != planFormatVersion { return nil, fmt.Errorf("unknown plan file version: %d", formatByte[0]) } @@ -112,7 +128,17 @@ func ReadPlan(src io.Reader) (*Plan, error) { // WritePlan writes a plan somewhere in a binary format. func WritePlan(d *Plan, dst io.Writer) error { - n, err := dst.Write([]byte{planFormatByte}) + // Write the magic bytes so we can determine the file format later + n, err := dst.Write([]byte(planFormatMagic)) + if err != nil { + return err + } + if n != len(planFormatMagic) { + return errors.New("failed to write plan format magic bytes") + } + + // Write a version byte so we can iterate on version at some point + n, err = dst.Write([]byte{planFormatVersion}) if err != nil { return err } From 9a6f1e594b4a3616279a0330f0ac7e1310a678b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 21:56:43 -0700 Subject: [PATCH 10/36] command/refresh: default state path, optional args --- command/refresh.go | 86 +++++++++++------------- command/refresh_test.go | 143 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 49 deletions(-) diff --git a/command/refresh.go b/command/refresh.go index a1229c9b7..420f84678 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -7,7 +7,6 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -20,68 +19,57 @@ type RefreshCommand struct { } func (c *RefreshCommand) Run(args []string) int { - var outPath string - statePath := "terraform.tfstate" - configPath := "." + var statePath, stateOutPath string cmdFlags := flag.NewFlagSet("refresh", flag.ContinueOnError) - cmdFlags.StringVar(&outPath, "out", "", "output path") + cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") + cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { return 1 } + var configPath string args = cmdFlags.Args() - if len(args) != 2 { - // TODO(mitchellh): this is temporary until we can assume current - // dir for Terraform config. - c.Ui.Error("TEMPORARY: The refresh command requires two args.") + if len(args) > 1 { + c.Ui.Error("The apply command expacts at most one argument.") cmdFlags.Usage() return 1 + } else if len(args) == 1 { + configPath = args[0] + } else { + var err error + configPath, err = os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + } } - statePath = args[0] - configPath = args[1] - if outPath == "" { - outPath = statePath + // If we don't specify an output path, default to out normal state + // path. + if stateOutPath == "" { + stateOutPath = statePath } - // Load up the state - f, err := os.Open(statePath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - - state, err := terraform.ReadState(f) - f.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - - b, err := config.Load(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading blueprint: %s", err)) - return 1 - } - - c.ContextOpts.Config = b - c.ContextOpts.State = state + // Build the context based on the arguments given c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) - ctx := terraform.NewContext(c.ContextOpts) + ctx, err := ContextArg(configPath, statePath, c.ContextOpts) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } if !validateContext(ctx, c.Ui) { return 1 } - state, err = ctx.Refresh() + state, err := ctx.Refresh() if err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) return 1 } - log.Printf("[INFO] Writing state output to: %s", outPath) - f, err = os.Create(outPath) + log.Printf("[INFO] Writing state output to: %s", stateOutPath) + f, err := os.Create(stateOutPath) if err == nil { defer f.Close() err = terraform.WriteState(state, f) @@ -96,21 +84,27 @@ func (c *RefreshCommand) Run(args []string) int { func (c *RefreshCommand) Help() string { helpText := ` -Usage: terraform refresh [options] [terraform.tfstate] [terraform.tf] +Usage: terraform refresh [options] [dir] - Refresh and update the state of your infrastructure. This is read-only - operation that will not modify infrastructure. The read-only property - is dependent on resource providers being implemented correctly. + Update the state file of your infrastructure with metadata that matches + the physical resources they are tracking. + + This will not modify your infrastructure, but it can modify your + state file to update metadata. This metadata might cause new changes + to occur when you generate a plan or call apply next. Options: - -out=path Path to write updated state file. If this is not specified, - the existing state file will be overridden. + -state=path Path to read and save state (unless state-out + is specified). Defaults to "terraform.tfstate". + + -state-out=path Path to write updated state file. By default, the + "-state" path will be used. ` return strings.TrimSpace(helpText) } func (c *RefreshCommand) Synopsis() string { - return "Refresh the state of your infrastructure" + return "Refresh the local state of your infrastructure" } diff --git a/command/refresh_test.go b/command/refresh_test.go index f6332c5ad..643b2daf6 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -3,6 +3,7 @@ package command import ( "io/ioutil" "os" + "path/filepath" "reflect" "testing" @@ -32,7 +33,7 @@ func TestRefresh(t *testing.T) { p.RefreshReturn = &terraform.ResourceState{ID: "yes"} args := []string{ - statePath, + "-state", statePath, testFixturePath("refresh"), } if code := c.Run(args); code != 0 { @@ -61,6 +62,142 @@ func TestRefresh(t *testing.T) { } } +func TestRefresh_cwd(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("refresh")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + state := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + p.RefreshFn = nil + p.RefreshReturn = &terraform.ResourceState{ID: "yes"} + + args := []string{ + "-state", statePath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := newState.Resources["test_instance.foo"] + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestRefresh_defaultState(t *testing.T) { + originalState := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + + // Write the state file in a temporary directory with the + // default filename. + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + statePath := filepath.Join(td, DefaultStateFilename) + + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(originalState, f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Change to that directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + p.RefreshFn = nil + p.RefreshReturn = &terraform.ResourceState{ID: "yes"} + + args := []string{ + testFixturePath("refresh"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !p.RefreshCalled { + t.Fatal("refresh should be called") + } + + f, err = os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + newState, err := terraform.ReadState(f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := newState.Resources["test_instance.foo"] + expected := p.RefreshReturn + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + func TestRefresh_outPath(t *testing.T) { state := &terraform.State{ Resources: map[string]*terraform.ResourceState{ @@ -92,8 +229,8 @@ func TestRefresh_outPath(t *testing.T) { p.RefreshReturn = &terraform.ResourceState{ID: "yes"} args := []string{ - "-out", outPath, - statePath, + "-state", statePath, + "-state-out", outPath, testFixturePath("refresh"), } if code := c.Run(args); code != 0 { From ff75d15f51ddfdc6141a3ce78a137d23526e753a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 22:00:55 -0700 Subject: [PATCH 11/36] command/refresh: better error message if bad state --- command/refresh.go | 24 ++++++++++++++++++++++++ command/refresh_test.go | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/command/refresh.go b/command/refresh.go index 420f84678..1164c7f8c 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -51,6 +51,30 @@ func (c *RefreshCommand) Run(args []string) int { stateOutPath = statePath } + // Verify that the state path exists + if _, err := os.Stat(statePath); err != nil { + if os.IsNotExist(err) { + c.Ui.Error(fmt.Sprintf( + "The Terraform state file for your infrastructure does not\n"+ + "exist. The 'refresh' command only works and only makes sense\n"+ + "when there is existing state that Terraform is managing. Please\n"+ + "double-check the value given below and try again. If you\n"+ + "haven't created infrastructure with Terraform yet, use the\n"+ + "'terraform apply' command.\n\n"+ + "Path: %s", + statePath)) + return 1 + } + + c.Ui.Error(fmt.Sprintf( + "There was an error reading the Terraform state that is needed\n"+ + "for refreshing. The path and error are shown below.\n\n"+ + "Path: %s\n\nError: %s", + statePath, + err)) + return 1 + } + // Build the context based on the arguments given c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) ctx, err := ContextArg(configPath, statePath, c.ContextOpts) diff --git a/command/refresh_test.go b/command/refresh_test.go index 643b2daf6..c7abc09c8 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -62,6 +62,23 @@ func TestRefresh(t *testing.T) { } } +func TestRefresh_badState(t *testing.T) { + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + ContextOpts: testCtxConfig(p), + Ui: ui, + } + + args := []string{ + "-state", "i-should-not-exist-ever", + testFixturePath("refresh"), + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + func TestRefresh_cwd(t *testing.T) { cwd, err := os.Getwd() if err != nil { From e39378b2543952d8e6179c1ca74c2457a2812677 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 22:01:33 -0700 Subject: [PATCH 12/36] command/refresh: better comment --- command/refresh.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/command/refresh.go b/command/refresh.go index 1164c7f8c..54c9ac320 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -51,7 +51,9 @@ func (c *RefreshCommand) Run(args []string) int { stateOutPath = statePath } - // Verify that the state path exists + // Verify that the state path exists. The "ContextArg" function below + // will actually do this, but we want to provide a richer error message + // if possible. if _, err := os.Stat(statePath); err != nil { if os.IsNotExist(err) { c.Ui.Error(fmt.Sprintf( From e476bca29c8f37c0ba705284f5789b76fae09cfe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Jul 2014 22:05:13 -0700 Subject: [PATCH 13/36] Add tfplan to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f5a7400d3..03a5621e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.dll *.exe example.tf +terraform.tfplan terraform.tfstate bin/ vendor/ From 832211c17a6ce41c22e849c0725a0b6861f21b85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 16:32:48 -0700 Subject: [PATCH 14/36] command/plan: nice plan formatting --- command/format_plan.go | 97 ++++++++++++++++++++++++++++++++++++++++++ command/hook_ui.go | 3 -- command/plan.go | 37 +++++++++++++++- 3 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 command/format_plan.go diff --git a/command/format_plan.go b/command/format_plan.go new file mode 100644 index 000000000..9c51ef01f --- /dev/null +++ b/command/format_plan.go @@ -0,0 +1,97 @@ +package command + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/colorstring" +) + +// FormatPlan takes a plan and returns a +func FormatPlan(p *terraform.Plan, c *colorstring.Colorize) string { + if c == nil { + c = &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Reset: false, + } + } + + buf := new(bytes.Buffer) + + // We want to output the resources in sorted order to make things + // easier to scan through, so get all the resource names and sort them. + names := make([]string, 0, len(p.Diff.Resources)) + for name, _ := range p.Diff.Resources { + names = append(names, name) + } + sort.Strings(names) + + // Go through each sorted name and start building the output + for _, name := range names { + rdiff := p.Diff.Resources[name] + + // Determine the color for the text (green for adding, yellow + // for change, red for delete), and symbol, and output the + // resource header. + color := "yellow" + symbol := "~" + if rdiff.RequiresNew() { + color = "green" + symbol = "+" + } else if rdiff.Destroy { + color = "red" + symbol = "-" + } + buf.WriteString(c.Color(fmt.Sprintf( + "[%s]%s %s\n", + color, symbol, name))) + + // Get all the attributes that are changing, and sort them. Also + // determine the longest key so that we can align them all. + keyLen := 0 + keys := make([]string, 0, len(rdiff.Attributes)) + for key, _ := range rdiff.Attributes { + // Skip the ID since we do that specially + if key == "id" { + continue + } + + keys = append(keys, key) + if len(key) > keyLen { + keyLen = len(key) + } + } + sort.Strings(keys) + + // Go through and output each attribute + for _, attrK := range keys { + attrDiff := rdiff.Attributes[attrK] + + v := attrDiff.New + if attrDiff.NewComputed { + v = "" + } + + newResource := "" + if attrDiff.RequiresNew && rdiff.Destroy { + newResource = " (forces new resource)" + } + + buf.WriteString(fmt.Sprintf( + " %s:%s %#v => %#v%s\n", + attrK, + strings.Repeat(" ", keyLen-len(attrK)), + attrDiff.Old, + v, + newResource)) + } + + // Write the reset color so we don't overload the user's terminal + buf.WriteString(c.Color("[reset]\n")) + } + + return strings.TrimSpace(buf.String()) +} diff --git a/command/hook_ui.go b/command/hook_ui.go index 2c298feba..e873f8a12 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -29,9 +29,6 @@ func (h *UiHook) PreApply( func (h *UiHook) PreDiff( id string, s *terraform.ResourceState) (terraform.HookAction, error) { - h.once.Do(h.init) - - h.ui.Output(fmt.Sprintf("%s: Calculating diff", id)) return terraform.HookActionContinue, nil } diff --git a/command/plan.go b/command/plan.go index d1c24482f..605cfd709 100644 --- a/command/plan.go +++ b/command/plan.go @@ -109,8 +109,6 @@ func (c *PlanCommand) Run(args []string) int { return 0 } - c.Ui.Output(strings.TrimSpace(plan.String())) - if outPath != "" { log.Printf("[INFO] Writing plan output to: %s", outPath) f, err := os.Create(outPath) @@ -124,6 +122,16 @@ func (c *PlanCommand) Run(args []string) int { } } + if outPath == "" { + c.Ui.Output(strings.TrimSpace(planHeaderNoOutput)+"\n") + } else { + c.Ui.Output(fmt.Sprintf( + strings.TrimSpace(planHeaderYesOutput)+"\n", + outPath)) + } + + c.Ui.Output(FormatPlan(plan, nil)) + return 0 } @@ -159,3 +167,28 @@ Options: func (c *PlanCommand) Synopsis() string { return "Show changes between Terraform config and infrastructure" } + +const planHeaderNoOutput = ` +The Terraform execution plan has been generated and is shown below. +Resources are shown in alphabetical order for quick scanning. Green resources +will be created (or destroyed and then created if an existing resource +exists), yellow resources are being changed in-place, and red resources +will be destroyed. + +Note: You didn't specify an "-out" parameter to save this plan, so when +"apply" is called, Terraform can't guarantee this is what will execute. +` + +const planHeaderYesOutput = ` +The Terraform execution plan has been generated and is shown below. +Resources are shown in alphabetical order for quick scanning. Green resources +will be created (or destroyed and then created if an existing resource +exists), yellow resources are being changed in-place, and red resources +will be destroyed. + +Your plan was also saved to the path below. Call the "apply" subcommand +with this plan file and Terraform will exactly execute this execution +plan. + +Path: %s +` From e0d3098d501d84bd1af2138971d0018b2d603f27 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 16:35:59 -0700 Subject: [PATCH 15/36] command/plan: better output --- command/plan.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/command/plan.go b/command/plan.go index 605cfd709..e91524dec 100644 --- a/command/plan.go +++ b/command/plan.go @@ -105,7 +105,11 @@ func (c *PlanCommand) Run(args []string) int { } if plan.Diff.Empty() { - c.Ui.Output("No changes. Infrastructure is up-to-date.") + c.Ui.Output( + "No changes. Infrastructure is up-to-date. This means that Terraform\n" + + "could not detect any differences between your configuration and\n" + + "the real physical resources that exist. As a result, Terraform\n" + + "doesn't need to do anything.") return 0 } @@ -123,7 +127,7 @@ func (c *PlanCommand) Run(args []string) int { } if outPath == "" { - c.Ui.Output(strings.TrimSpace(planHeaderNoOutput)+"\n") + c.Ui.Output(strings.TrimSpace(planHeaderNoOutput) + "\n") } else { c.Ui.Output(fmt.Sprintf( strings.TrimSpace(planHeaderYesOutput)+"\n", From c615afc0976121ccef6c8967b68df420a3497b3d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 17:03:55 -0700 Subject: [PATCH 16/36] command/apply: improved output, still not done --- command/apply.go | 3 ++ command/format_plan.go | 2 +- command/hook_ui.go | 66 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/command/apply.go b/command/apply.go index 636a8daa7..0b7d36243 100644 --- a/command/apply.go +++ b/command/apply.go @@ -71,6 +71,7 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } + // Start the apply in a goroutine so that we can be interrupted. var state *terraform.State var applyErr error doneCh := make(chan struct{}) @@ -79,6 +80,8 @@ func (c *ApplyCommand) Run(args []string) int { state, applyErr = ctx.Apply() }() + // Wait for the apply to finish or for us to be interrupted so + // we can handle it properly. err = nil select { case <-c.ShutdownCh: diff --git a/command/format_plan.go b/command/format_plan.go index 9c51ef01f..6b7e69485 100644 --- a/command/format_plan.go +++ b/command/format_plan.go @@ -81,7 +81,7 @@ func FormatPlan(p *terraform.Plan, c *colorstring.Colorize) string { } buf.WriteString(fmt.Sprintf( - " %s:%s %#v => %#v%s\n", + " %s:%s %#v => %#v%s\n", attrK, strings.Repeat(" ", keyLen-len(attrK)), attrDiff.Old, diff --git a/command/hook_ui.go b/command/hook_ui.go index e873f8a12..434fae020 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -1,17 +1,22 @@ package command import ( + "bytes" "fmt" + "sort" + "strings" "sync" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" ) type UiHook struct { terraform.NilHook - Ui cli.Ui + Colorize *colorstring.Colorize + Ui cli.Ui once sync.Once ui cli.Ui @@ -23,7 +28,55 @@ func (h *UiHook) PreApply( d *terraform.ResourceDiff) (terraform.HookAction, error) { h.once.Do(h.init) - h.ui.Output(fmt.Sprintf("%s: Applying...", id)) + operation := "Modifying..." + if d.Destroy { + operation = "Destroying..." + } else if s.ID == "" { + operation = "Creating..." + } + + attrBuf := new(bytes.Buffer) + + // Get all the attributes that are changing, and sort them. Also + // determine the longest key so that we can align them all. + keyLen := 0 + keys := make([]string, 0, len(d.Attributes)) + for key, _ := range d.Attributes { + // Skip the ID since we do that specially + if key == "id" { + continue + } + + keys = append(keys, key) + if len(key) > keyLen { + keyLen = len(key) + } + } + sort.Strings(keys) + + // Go through and output each attribute + for _, attrK := range keys { + attrDiff := d.Attributes[attrK] + + v := attrDiff.New + if attrDiff.NewComputed { + v = "" + } + + attrBuf.WriteString(fmt.Sprintf( + " %s:%s %#v => %#v\n", + attrK, + strings.Repeat(" ", keyLen-len(attrK)), + attrDiff.Old, + v)) + } + + h.ui.Output(h.Colorize.Color(fmt.Sprintf( + "[bold]%s: %s[reset_bold]\n %s", + id, + operation, + strings.TrimSpace(attrBuf.String())))) + return terraform.HookActionContinue, nil } @@ -36,11 +89,18 @@ func (h *UiHook) PreRefresh( id string, s *terraform.ResourceState) (terraform.HookAction, error) { h.once.Do(h.init) - h.ui.Output(fmt.Sprintf("%s: Refreshing state (ID: %s)", id, s.ID)) + //h.ui.Output(fmt.Sprintf("%s: Refreshing state (ID: %s)", id, s.ID)) return terraform.HookActionContinue, nil } func (h *UiHook) init() { + if h.Colorize == nil { + h.Colorize = &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Reset: true, + } + } + // Wrap the ui so that it is safe for concurrency regardless of the // underlying reader/writer that is in place. h.ui = &cli.ConcurrentUi{Ui: h.Ui} From f085ae65fa115d2e9707279a94e6441b154279f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 17:12:41 -0700 Subject: [PATCH 17/36] providers/aws: fix issue where default route was being added --- .../providers/aws/resource_aws_route_table.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route_table.go b/builtin/providers/aws/resource_aws_route_table.go index 12a0fffe1..7ba631903 100644 --- a/builtin/providers/aws/resource_aws_route_table.go +++ b/builtin/providers/aws/resource_aws_route_table.go @@ -110,12 +110,18 @@ func resource_aws_route_table_update( break } - // Append to the routes what we've done so far - resultRoutes = append(resultRoutes, map[string]string{ - "cidr_block": op.Route.DestinationCidrBlock, - "gateway_id": op.Route.GatewayId, - "instance_id": op.Route.InstanceId, - }) + // If we didn't delete the route, append it to the list of routes + // we have. + if op.Op != routeTableOpDelete { + resultMap := map[string]string{"cidr_block": op.Route.DestinationCidrBlock} + if op.Route.GatewayId != "" { + resultMap["gateway_id"] = op.Route.GatewayId + } else if op.Route.InstanceId != "" { + resultMap["instance_id"] = op.Route.InstanceId + } + + resultRoutes = append(resultRoutes, resultMap) + } } // Update our state with the settings From b4b116a0185e3c4a808d017514a4fe59f89cd844 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 17:17:03 -0700 Subject: [PATCH 18/36] command/plan: output while refreshing since that can take awhile --- command/hook_ui.go | 6 ++++-- command/plan.go | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/command/hook_ui.go b/command/hook_ui.go index 434fae020..e642e55ed 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -72,7 +72,7 @@ func (h *UiHook) PreApply( } h.ui.Output(h.Colorize.Color(fmt.Sprintf( - "[bold]%s: %s[reset_bold]\n %s", + "[reset][bold]%s: %s[reset_bold]\n %s", id, operation, strings.TrimSpace(attrBuf.String())))) @@ -89,7 +89,9 @@ func (h *UiHook) PreRefresh( id string, s *terraform.ResourceState) (terraform.HookAction, error) { h.once.Do(h.init) - //h.ui.Output(fmt.Sprintf("%s: Refreshing state (ID: %s)", id, s.ID)) + h.ui.Output(h.Colorize.Color(fmt.Sprintf( + "[reset][bold]%s: Refreshing (ID: %s)", + id, s.ID))) return terraform.HookActionContinue, nil } diff --git a/command/plan.go b/command/plan.go index e91524dec..c6115a923 100644 --- a/command/plan.go +++ b/command/plan.go @@ -92,10 +92,12 @@ func (c *PlanCommand) Run(args []string) int { } if refresh { + c.Ui.Output("Refreshing Terraform state prior to plan...\n") if _, err := ctx.Refresh(); err != nil { c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) return 1 } + c.Ui.Output("") } plan, err := ctx.Plan(&terraform.PlanOpts{Destroy: destroy}) From 4dea4c325b28127862780fcbdad478d55fdc437d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 19:23:56 -0700 Subject: [PATCH 19/36] terraform: Context.Graph --- terraform/context.go | 5 +++++ terraform/context_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/terraform/context.go b/terraform/context.go index 5d6c07291..bf1980045 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -127,6 +127,11 @@ func (c *Context) Apply() (*State, error) { return c.state, err } +// Graph returns the graph for this context. +func (c *Context) Graph() (*depgraph.Graph, error) { + return c.graph() +} + // Plan generates an execution plan for the given context. // // The execution plan encapsulates the context and can be stored diff --git a/terraform/context_test.go b/terraform/context_test.go index 8051ac4b2..65e472de9 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -7,6 +7,28 @@ import ( "testing" ) +func TestContextGraph(t *testing.T) { + p := testProvider("aws") + config := testConfig(t, "validate-good") + c := testContext(t, &ContextOpts{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + g, err := c.Graph() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testContextGraph) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + func TestContextValidate(t *testing.T) { p := testProvider("aws") config := testConfig(t, "validate-good") @@ -1605,3 +1627,15 @@ func testProvisioner() *MockResourceProvisioner { p := new(MockResourceProvisioner) return p } + +const testContextGraph = ` +root: root +aws_instance.bar + aws_instance.bar -> provider.aws +aws_instance.foo + aws_instance.foo -> provider.aws +provider.aws +root + root -> aws_instance.bar + root -> aws_instance.foo +` From 8e100869a425e783a4e376a0d713d143d8d54747 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 19:25:50 -0700 Subject: [PATCH 20/36] command/graph: can graph plans --- command/graph.go | 10 +++------- command/graph_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/command/graph.go b/command/graph.go index aec4ffecd..42d59a2a6 100644 --- a/command/graph.go +++ b/command/graph.go @@ -7,7 +7,6 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/digraph" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -43,16 +42,13 @@ func (c *GraphCommand) Run(args []string) int { } } - conf, err := config.LoadDir(path) + ctx, err := ContextArg(path, "", c.ContextOpts) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) + c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) return 1 } - g, err := terraform.Graph(&terraform.GraphOpts{ - Config: conf, - Providers: c.ContextOpts.Providers, - }) + g, err := ctx.Graph() if err != nil { c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err)) return 1 diff --git a/command/graph_test.go b/command/graph_test.go index 1a1e298f9..f95089def 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -70,3 +72,27 @@ func TestGraph_noArgs(t *testing.T) { t.Fatalf("doesn't look like digraph: %s", output) } } + +func TestGraph_plan(t *testing.T) { + planPath := testPlanFile(t, &terraform.Plan{ + Config: new(config.Config), + }) + + ui := new(cli.MockUi) + c := &GraphCommand{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + } + + args := []string{ + planPath, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "digraph {") { + t.Fatalf("doesn't look like digraph: %s", output) + } +} From 3d351581701c6b2fda86c872bd6f0a68bd015225 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 19:28:38 -0700 Subject: [PATCH 21/36] command: update synopsis to be better --- command/graph.go | 2 +- command/plan.go | 2 +- command/refresh.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/graph.go b/command/graph.go index 42d59a2a6..791f074f6 100644 --- a/command/graph.go +++ b/command/graph.go @@ -84,5 +84,5 @@ Usage: terraform graph [options] PATH } func (c *GraphCommand) Synopsis() string { - return "Output visual graph of Terraform resources" + return "Create a visual graph of Terraform resources" } diff --git a/command/plan.go b/command/plan.go index c6115a923..88ae70909 100644 --- a/command/plan.go +++ b/command/plan.go @@ -171,7 +171,7 @@ Options: } func (c *PlanCommand) Synopsis() string { - return "Show changes between Terraform config and infrastructure" + return "Generate and show an execution plan" } const planHeaderNoOutput = ` diff --git a/command/refresh.go b/command/refresh.go index 54c9ac320..137aadb1c 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -132,5 +132,5 @@ Options: } func (c *RefreshCommand) Synopsis() string { - return "Refresh the local state of your infrastructure" + return "Update local state file against real resources" } From dbc1c63d799c13393e48c7e07a194ab8a3702451 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 19:47:31 -0700 Subject: [PATCH 22/36] command/show --- command/format_plan.go | 4 ++ command/format_state.go | 85 ++++++++++++++++++++++++++++++++++++ command/graph_test.go | 2 +- command/show.go | 96 +++++++++++++++++++++++++++++++++++++++++ command/show_test.go | 83 +++++++++++++++++++++++++++++++++++ commands.go | 7 +++ 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 command/format_state.go create mode 100644 command/show.go create mode 100644 command/show_test.go 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, From 6c736bd3c41a5c32fb78615f57b3441d65800aef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 20:21:46 -0700 Subject: [PATCH 23/36] command: introduce Meta and "-no-color" option --- command/apply.go | 6 ++++++ command/command.go | 13 +++++++++++++ command/format_state.go | 5 +---- command/hook_ui.go | 5 +---- command/meta.go | 35 +++++++++++++++++++++++++++++++++++ command/meta_test.go | 35 +++++++++++++++++++++++++++++++++++ command/plan.go | 8 +++++++- command/refresh.go | 6 ++++++ command/show.go | 12 ++++++++++-- 9 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 command/meta.go create mode 100644 command/meta_test.go diff --git a/command/apply.go b/command/apply.go index 0b7d36243..b27483aed 100644 --- a/command/apply.go +++ b/command/apply.go @@ -13,6 +13,8 @@ import ( // ApplyCommand is a Command implementation that applies a Terraform // configuration and actually builds or changes infrastructure. type ApplyCommand struct { + Meta + ShutdownCh <-chan struct{} ContextOpts *terraform.ContextOpts Ui cli.Ui @@ -22,6 +24,8 @@ func (c *ApplyCommand) Run(args []string) int { var init bool var statePath, stateOutPath string + args = c.Meta.process(args) + cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError) cmdFlags.BoolVar(&init, "init", false, "init") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") @@ -139,6 +143,8 @@ Options: to prevent accidentally spinning up a new infrastructure. + -no-color If specified, output won't contain any color. + -state=path Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate". diff --git a/command/command.go b/command/command.go index b8f4aab9d..cef5c2924 100644 --- a/command/command.go +++ b/command/command.go @@ -7,11 +7,24 @@ import ( "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" ) // DefaultStateFilename is the default filename used for the state file. const DefaultStateFilename = "terraform.tfstate" +// Colorize returns our default coloring settings for strings. +func Colorize() *colorstring.Colorize { + return &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Reset: true, + } +} + +// ContextArg is a helper function that creates a context based on +// the arguments given. If the path is a plan file, it creates a context +// from that. Otherwise, it creates a context based on the state file +// (if given). func ContextArg( path string, statePath string, diff --git a/command/format_state.go b/command/format_state.go index 2b31a9b51..f63e1e352 100644 --- a/command/format_state.go +++ b/command/format_state.go @@ -17,10 +17,7 @@ func FormatState(s *terraform.State, c *colorstring.Colorize) string { } if c == nil { - c = &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Reset: false, - } + c = Colorize() } buf := new(bytes.Buffer) diff --git a/command/hook_ui.go b/command/hook_ui.go index e642e55ed..8f7c26e8c 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -97,10 +97,7 @@ func (h *UiHook) PreRefresh( func (h *UiHook) init() { if h.Colorize == nil { - h.Colorize = &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Reset: true, - } + h.Colorize = Colorize() } // Wrap the ui so that it is safe for concurrency regardless of the diff --git a/command/meta.go b/command/meta.go new file mode 100644 index 000000000..ee16f45c0 --- /dev/null +++ b/command/meta.go @@ -0,0 +1,35 @@ +package command + +import ( + "github.com/mitchellh/colorstring" +) + +// Meta are the meta-options that are available on all or most commands. +type Meta struct { + Color bool +} + +// Colorize returns the colorization structure for a command. +func (m *Meta) Colorize() *colorstring.Colorize { + return &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: !m.Color, + Reset: true, + } +} + +// process will process the meta-parameters out of the arguments. This +// will potentially modify the args in-place. It will return the resulting +// slice. +func (m *Meta) process(args []string) []string { + m.Color = true + + for i, v := range args { + if v == "-no-color" { + m.Color = false + return append(args[:i], args[i+1:]...) + } + } + + return args +} diff --git a/command/meta_test.go b/command/meta_test.go new file mode 100644 index 000000000..6a2f680f3 --- /dev/null +++ b/command/meta_test.go @@ -0,0 +1,35 @@ +package command + +import ( + "reflect" + "testing" +) + +func TestMetaColorize(t *testing.T) { + var m *Meta + var args, args2 []string + + // Test basic, no change + m = new(Meta) + args = []string{"foo", "bar"} + args2 = []string{"foo", "bar"} + args = m.process(args) + if !reflect.DeepEqual(args, args2) { + t.Fatalf("bad: %#v", args) + } + if m.Colorize().Disable { + t.Fatal("should not be disabled") + } + + // Test disable #1 + m = new(Meta) + args = []string{"foo", "-no-color", "bar"} + args2 = []string{"foo", "bar"} + args = m.process(args) + if !reflect.DeepEqual(args, args2) { + t.Fatalf("bad: %#v", args) + } + if !m.Colorize().Disable { + t.Fatal("should be disabled") + } +} diff --git a/command/plan.go b/command/plan.go index 88ae70909..b1a77f7d5 100644 --- a/command/plan.go +++ b/command/plan.go @@ -15,6 +15,8 @@ import ( // PlanCommand is a Command implementation that compares a Terraform // configuration to an actual infrastructure and shows the differences. type PlanCommand struct { + Meta + ContextOpts *terraform.ContextOpts Ui cli.Ui } @@ -23,6 +25,8 @@ func (c *PlanCommand) Run(args []string) int { var destroy, refresh bool var outPath, statePath string + args = c.Meta.process(args) + cmdFlags := flag.NewFlagSet("plan", flag.ContinueOnError) cmdFlags.BoolVar(&destroy, "destroy", false, "destroy") cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") @@ -136,7 +140,7 @@ func (c *PlanCommand) Run(args []string) int { outPath)) } - c.Ui.Output(FormatPlan(plan, nil)) + c.Ui.Output(FormatPlan(plan, c.Colorize())) return 0 } @@ -157,6 +161,8 @@ Options: -destroy If set, a plan will be generated to destroy all resources managed by the given configuration and state. + -no-color If specified, output won't contain any color. + -out=path Write a plan file to the given path. This can be used as input to the "apply" command. diff --git a/command/refresh.go b/command/refresh.go index 137aadb1c..56a63c572 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -14,6 +14,8 @@ import ( // RefreshCommand is a cli.Command implementation that refreshes the state // file. type RefreshCommand struct { + Meta + ContextOpts *terraform.ContextOpts Ui cli.Ui } @@ -21,6 +23,8 @@ type RefreshCommand struct { func (c *RefreshCommand) Run(args []string) int { var statePath, stateOutPath string + args = c.Meta.process(args) + cmdFlags := flag.NewFlagSet("refresh", flag.ContinueOnError) cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") @@ -121,6 +125,8 @@ Usage: terraform refresh [options] [dir] Options: + -no-color If specified, output won't contain any color. + -state=path Path to read and save state (unless state-out is specified). Defaults to "terraform.tfstate". diff --git a/command/show.go b/command/show.go index 0d5ebce59..c70e2b2ff 100644 --- a/command/show.go +++ b/command/show.go @@ -13,11 +13,15 @@ import ( // ShowCommand is a Command implementation that reads and outputs the // contents of a Terraform plan or state file. type ShowCommand struct { + Meta + ContextOpts *terraform.ContextOpts Ui cli.Ui } func (c *ShowCommand) Run(args []string) int { + args = c.Meta.process(args) + cmdFlags := flag.NewFlagSet("show", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { @@ -72,11 +76,11 @@ func (c *ShowCommand) Run(args []string) int { } if plan != nil { - c.Ui.Output(FormatPlan(plan, nil)) + c.Ui.Output(FormatPlan(plan, c.Colorize())) return 0 } - c.Ui.Output(FormatState(state, nil)) + c.Ui.Output(FormatState(state, c.Colorize())) return 0 } @@ -87,6 +91,10 @@ Usage: terraform show [options] path Reads and outputs a Terraform state or plan file in a human-readable form. +Options: + + -no-color If specified, output won't contain any color. + ` return strings.TrimSpace(helpText) } From 3a851bece0febdd97ff3007877e75fea13d9ada2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 20:37:30 -0700 Subject: [PATCH 24/36] command: convert all to use the new Meta thing --- command/apply.go | 8 +--- command/apply_test.go | 57 ++++++++++++++++--------- command/command.go | 77 ---------------------------------- command/format_state.go | 8 ++-- command/graph.go | 9 ++-- command/graph_test.go | 24 +++++++---- command/hook_ui.go | 2 +- command/meta.go | 92 ++++++++++++++++++++++++++++++++++++++++- command/plan.go | 31 +------------- command/plan_test.go | 42 ++++++++++++------- command/refresh.go | 7 +--- command/refresh_test.go | 30 +++++++++----- 12 files changed, 207 insertions(+), 180 deletions(-) diff --git a/command/apply.go b/command/apply.go index b27483aed..d87e1954e 100644 --- a/command/apply.go +++ b/command/apply.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" ) // ApplyCommand is a Command implementation that applies a Terraform @@ -15,9 +14,7 @@ import ( type ApplyCommand struct { Meta - ShutdownCh <-chan struct{} - ContextOpts *terraform.ContextOpts - Ui cli.Ui + ShutdownCh <-chan struct{} } func (c *ApplyCommand) Run(args []string) int { @@ -65,8 +62,7 @@ func (c *ApplyCommand) Run(args []string) int { } // Build the context based on the arguments given - c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) - ctx, err := ContextArg(configPath, planStatePath, c.ContextOpts) + ctx, err := c.Context(configPath, planStatePath) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/apply_test.go b/command/apply_test.go index 58a831a2f..48a1a1c70 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -21,8 +21,10 @@ func TestApply(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -57,8 +59,10 @@ func TestApply_configInvalid(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -91,8 +95,10 @@ func TestApply_defaultState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -128,8 +134,10 @@ func TestApply_error(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } var lock sync.Mutex @@ -205,8 +213,10 @@ func TestApply_noArgs(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -245,8 +255,10 @@ func TestApply_plan(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -287,9 +299,12 @@ func TestApply_shutdown(t *testing.T) { shutdownCh := make(chan struct{}) ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - ShutdownCh: shutdownCh, - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + + ShutdownCh: shutdownCh, } p.DiffFn = func( @@ -387,8 +402,10 @@ func TestApply_state(t *testing.T) { ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } // Run the apply command pointing to our existing state @@ -434,8 +451,10 @@ func TestApply_stateNoExist(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ diff --git a/command/command.go b/command/command.go index cef5c2924..9d27321d5 100644 --- a/command/command.go +++ b/command/command.go @@ -2,91 +2,14 @@ package command import ( "fmt" - "os" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" - "github.com/mitchellh/colorstring" ) // DefaultStateFilename is the default filename used for the state file. const DefaultStateFilename = "terraform.tfstate" -// Colorize returns our default coloring settings for strings. -func Colorize() *colorstring.Colorize { - return &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Reset: true, - } -} - -// ContextArg is a helper function that creates a context based on -// the arguments given. If the path is a plan file, it creates a context -// from that. Otherwise, it creates a context based on the state file -// (if given). -func ContextArg( - path string, - statePath string, - opts *terraform.ContextOpts) (*terraform.Context, error) { - // First try to just read the plan directly from the path given. - f, err := os.Open(path) - if err == nil { - plan, err := terraform.ReadPlan(f) - f.Close() - if err == nil { - return plan.Context(opts), nil - } - } - - if statePath != "" { - if _, err := os.Stat(statePath); err != nil { - return nil, fmt.Errorf( - "There was an error reading the state file. The path\n"+ - "and error are shown below. If you're trying to build a\n"+ - "brand new infrastructure, explicitly pass the '-init'\n"+ - "flag to Terraform to tell it it is okay to build new\n"+ - "state.\n\n"+ - "Path: %s\n"+ - "Error: %s", - statePath, - err) - } - } - - // Load up the state - var state *terraform.State - if statePath != "" { - f, err := os.Open(statePath) - if err == nil { - state, err = terraform.ReadState(f) - f.Close() - } - - if err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) - } - } - - config, err := config.LoadDir(path) - if err != nil { - return nil, fmt.Errorf("Error loading config: %s", err) - } - if err := config.Validate(); err != nil { - return nil, fmt.Errorf("Error validating config: %s", err) - } - - opts.Config = config - opts.State = state - ctx := terraform.NewContext(opts) - - if _, err := ctx.Plan(nil); err != nil { - return nil, fmt.Errorf("Error running plan: %s", err) - } - - return ctx, nil -} - func validateContext(ctx *terraform.Context, ui cli.Ui) bool { if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { ui.Output( diff --git a/command/format_state.go b/command/format_state.go index f63e1e352..59f02e590 100644 --- a/command/format_state.go +++ b/command/format_state.go @@ -12,12 +12,12 @@ import ( // 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 { + panic("colorize not given") } - if c == nil { - c = Colorize() + if len(s.Resources) == 0 { + return "The state file is empty. No resources are represented." } buf := new(bytes.Buffer) diff --git a/command/graph.go b/command/graph.go index 791f074f6..f0c4bcf3f 100644 --- a/command/graph.go +++ b/command/graph.go @@ -8,18 +8,17 @@ import ( "strings" "github.com/hashicorp/terraform/digraph" - "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" ) // GraphCommand is a Command implementation that takes a Terraform // configuration and outputs the dependency tree in graphical form. type GraphCommand struct { - ContextOpts *terraform.ContextOpts - Ui cli.Ui + Meta } func (c *GraphCommand) Run(args []string) int { + args = c.Meta.process(args) + cmdFlags := flag.NewFlagSet("graph", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { @@ -42,7 +41,7 @@ func (c *GraphCommand) Run(args []string) int { } } - ctx, err := ContextArg(path, "", c.ContextOpts) + ctx, err := c.Context(path, "") if err != nil { c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) return 1 diff --git a/command/graph_test.go b/command/graph_test.go index 0a74726e7..cbc564e8f 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -13,8 +13,10 @@ import ( func TestGraph(t *testing.T) { ui := new(cli.MockUi) c := &GraphCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ @@ -33,8 +35,10 @@ func TestGraph(t *testing.T) { func TestGraph_multipleArgs(t *testing.T) { ui := new(cli.MockUi) c := &GraphCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ @@ -58,8 +62,10 @@ func TestGraph_noArgs(t *testing.T) { ui := new(cli.MockUi) c := &GraphCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{} @@ -80,8 +86,10 @@ func TestGraph_plan(t *testing.T) { ui := new(cli.MockUi) c := &GraphCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ diff --git a/command/hook_ui.go b/command/hook_ui.go index 8f7c26e8c..3f4d9bf5f 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -97,7 +97,7 @@ func (h *UiHook) PreRefresh( func (h *UiHook) init() { if h.Colorize == nil { - h.Colorize = Colorize() + panic("colorize not given") } // Wrap the ui so that it is safe for concurrency regardless of the diff --git a/command/meta.go b/command/meta.go index ee16f45c0..a7758a444 100644 --- a/command/meta.go +++ b/command/meta.go @@ -1,12 +1,20 @@ package command import ( + "fmt" + "os" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" ) // Meta are the meta-options that are available on all or most commands. type Meta struct { - Color bool + Color bool + ContextOpts *terraform.ContextOpts + Ui cli.Ui } // Colorize returns the colorization structure for a command. @@ -18,6 +26,80 @@ func (m *Meta) Colorize() *colorstring.Colorize { } } +// Context returns a Terraform Context taking into account the context +// options used to initialize this meta configuration. +func (m *Meta) Context(path, statePath string) (*terraform.Context, error) { + opts := m.contextOpts() + + // First try to just read the plan directly from the path given. + f, err := os.Open(path) + if err == nil { + plan, err := terraform.ReadPlan(f) + f.Close() + if err == nil { + return plan.Context(opts), nil + } + } + + if statePath != "" { + if _, err := os.Stat(statePath); err != nil { + return nil, fmt.Errorf( + "There was an error reading the state file. The path\n"+ + "and error are shown below. If you're trying to build a\n"+ + "brand new infrastructure, explicitly pass the '-init'\n"+ + "flag to Terraform to tell it it is okay to build new\n"+ + "state.\n\n"+ + "Path: %s\n"+ + "Error: %s", + statePath, + err) + } + } + + // Load up the state + var state *terraform.State + if statePath != "" { + f, err := os.Open(statePath) + if err == nil { + state, err = terraform.ReadState(f) + f.Close() + } + + if err != nil { + return nil, fmt.Errorf("Error loading state: %s", err) + } + } + + config, err := config.LoadDir(path) + if err != nil { + return nil, fmt.Errorf("Error loading config: %s", err) + } + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("Error validating config: %s", err) + } + + opts.Config = config + opts.State = state + ctx := terraform.NewContext(opts) + + if _, err := ctx.Plan(nil); err != nil { + return nil, fmt.Errorf("Error running plan: %s", err) + } + + return ctx, nil + +} + +// contextOpts returns the options to use to initialize a Terraform +// context with the settings from this Meta. +func (m *Meta) contextOpts() *terraform.ContextOpts { + var opts terraform.ContextOpts = *m.ContextOpts + opts.Hooks = make([]terraform.Hook, len(m.ContextOpts.Hooks)+1) + opts.Hooks[0] = m.uiHook() + copy(opts.Hooks[1:], m.ContextOpts.Hooks) + return &opts +} + // process will process the meta-parameters out of the arguments. This // will potentially modify the args in-place. It will return the resulting // slice. @@ -33,3 +115,11 @@ func (m *Meta) process(args []string) []string { return args } + +// uiHook returns the UiHook to use with the context. +func (m *Meta) uiHook() *UiHook { + return &UiHook{ + Colorize: m.Colorize(), + Ui: m.Ui, + } +} diff --git a/command/plan.go b/command/plan.go index b1a77f7d5..44790f72b 100644 --- a/command/plan.go +++ b/command/plan.go @@ -7,18 +7,13 @@ import ( "os" "strings" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" ) // PlanCommand is a Command implementation that compares a Terraform // configuration to an actual infrastructure and shows the differences. type PlanCommand struct { Meta - - ContextOpts *terraform.ContextOpts - Ui cli.Ui } func (c *PlanCommand) Run(args []string) int { @@ -64,33 +59,11 @@ func (c *PlanCommand) Run(args []string) int { } } - // Load up the state - var state *terraform.State - if statePath != "" { - f, err := os.Open(statePath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - - state, err = terraform.ReadState(f) - f.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) - return 1 - } - } - - b, err := config.LoadDir(path) + ctx, err := c.Context(path, statePath) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading config: %s", err)) + c.Ui.Error(err.Error()) return 1 } - - c.ContextOpts.Config = b - c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) - c.ContextOpts.State = state - ctx := terraform.NewContext(c.ContextOpts) if !validateContext(ctx, c.Ui) { return 1 } diff --git a/command/plan_test.go b/command/plan_test.go index f31c82356..473f68a0c 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -24,8 +24,10 @@ func TestPlan(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{} @@ -50,8 +52,10 @@ func TestPlan_destroy(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -79,8 +83,10 @@ func TestPlan_noState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -115,8 +121,10 @@ func TestPlan_outPath(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } p.DiffReturn = &terraform.ResourceDiff{ @@ -146,8 +154,10 @@ func TestPlan_refresh(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -190,8 +200,10 @@ func TestPlan_state(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -250,8 +262,10 @@ func TestPlan_stateDefault(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &PlanCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ diff --git a/command/refresh.go b/command/refresh.go index 56a63c572..256bf70dd 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -8,16 +8,12 @@ import ( "strings" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/cli" ) // RefreshCommand is a cli.Command implementation that refreshes the state // file. type RefreshCommand struct { Meta - - ContextOpts *terraform.ContextOpts - Ui cli.Ui } func (c *RefreshCommand) Run(args []string) int { @@ -82,8 +78,7 @@ func (c *RefreshCommand) Run(args []string) int { } // Build the context based on the arguments given - c.ContextOpts.Hooks = append(c.ContextOpts.Hooks, &UiHook{Ui: c.Ui}) - ctx, err := ContextArg(configPath, statePath, c.ContextOpts) + ctx, err := c.Context(configPath, statePath) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/refresh_test.go b/command/refresh_test.go index c7abc09c8..7acf39e5f 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -25,8 +25,10 @@ func TestRefresh(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } p.RefreshFn = nil @@ -66,8 +68,10 @@ func TestRefresh_badState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } args := []string{ @@ -102,8 +106,10 @@ func TestRefresh_cwd(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } p.RefreshFn = nil @@ -179,8 +185,10 @@ func TestRefresh_defaultState(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } p.RefreshFn = nil @@ -238,8 +246,10 @@ func TestRefresh_outPath(t *testing.T) { p := testProvider() ui := new(cli.MockUi) c := &RefreshCommand{ - ContextOpts: testCtxConfig(p), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, } p.RefreshFn = nil From 77bfa5657eba80d1d28b04fc3f3a95d9abd66579 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 20:38:56 -0700 Subject: [PATCH 25/36] Fix compilation, use the new command.Meta object --- commands.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/commands.go b/commands.go index d7aa46eb9..ee0dc69ba 100644 --- a/commands.go +++ b/commands.go @@ -26,40 +26,40 @@ func init() { Ui: &cli.BasicUi{Writer: os.Stdout}, } + meta := command.Meta{ + ContextOpts: &ContextOpts, + Ui: Ui, + } + Commands = map[string]cli.CommandFactory{ "apply": func() (cli.Command, error) { return &command.ApplyCommand{ - ShutdownCh: makeShutdownCh(), - ContextOpts: &ContextOpts, - Ui: Ui, + Meta: meta, + ShutdownCh: makeShutdownCh(), }, nil }, "graph": func() (cli.Command, error) { return &command.GraphCommand{ - ContextOpts: &ContextOpts, - Ui: Ui, + Meta: meta, }, nil }, "plan": func() (cli.Command, error) { return &command.PlanCommand{ - ContextOpts: &ContextOpts, - Ui: Ui, + Meta: meta, }, nil }, "refresh": func() (cli.Command, error) { return &command.RefreshCommand{ - ContextOpts: &ContextOpts, - Ui: Ui, + Meta: meta, }, nil }, "show": func() (cli.Command, error) { return &command.ShowCommand{ - ContextOpts: &ContextOpts, - Ui: Ui, + Meta: meta, }, nil }, From 79c60e033147a761f59ebcae6297b33a6fce9982 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 20:59:16 -0700 Subject: [PATCH 26/36] command/*: colors on error messages (red) --- command/cli_ui.go | 42 ++++++++++++++++++++++++++++++++++++++++++ command/cli_ui_test.go | 11 +++++++++++ command/meta.go | 21 +++++++++++++++++++-- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 command/cli_ui.go create mode 100644 command/cli_ui_test.go diff --git a/command/cli_ui.go b/command/cli_ui.go new file mode 100644 index 000000000..b9fc23556 --- /dev/null +++ b/command/cli_ui.go @@ -0,0 +1,42 @@ +package command + +import ( + "fmt" + + "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" +) + +// ColoredUi is a Ui implementation that colors its output according +// to the given color schemes for the given type of output. +type ColorizeUi struct { + Colorize *colorstring.Colorize + OutputColor string + InfoColor string + ErrorColor string + Ui cli.Ui +} + +func (u *ColorizeUi) Ask(query string) (string, error) { + return u.Ui.Ask(u.colorize(query, u.OutputColor)) +} + +func (u *ColorizeUi) Output(message string) { + u.Ui.Output(u.colorize(message, u.OutputColor)) +} + +func (u *ColorizeUi) Info(message string) { + u.Ui.Info(u.colorize(message, u.InfoColor)) +} + +func (u *ColorizeUi) Error(message string) { + u.Ui.Error(u.colorize(message, u.ErrorColor)) +} + +func (u *ColorizeUi) colorize(message string, color string) string { + if color == "" { + return message + } + + return u.Colorize.Color(fmt.Sprintf("%s%s[reset]", color, message)) +} diff --git a/command/cli_ui_test.go b/command/cli_ui_test.go new file mode 100644 index 000000000..ac2b7d7ea --- /dev/null +++ b/command/cli_ui_test.go @@ -0,0 +1,11 @@ +package command + +import ( + "testing" + + "github.com/mitchellh/cli" +) + +func TestColorizeUi_impl(t *testing.T) { + var _ cli.Ui = new(ColorizeUi) +} diff --git a/command/meta.go b/command/meta.go index a7758a444..3bafdb7a8 100644 --- a/command/meta.go +++ b/command/meta.go @@ -15,6 +15,8 @@ type Meta struct { Color bool ContextOpts *terraform.ContextOpts Ui cli.Ui + + oldUi cli.Ui } // Colorize returns the colorization structure for a command. @@ -104,15 +106,30 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { // will potentially modify the args in-place. It will return the resulting // slice. func (m *Meta) process(args []string) []string { - m.Color = true + // We do this so that we retain the ability to technically call + // process multiple times, even if we have no plans to do so + if m.oldUi != nil { + m.Ui = m.oldUi + } + // Set colorization + m.Color = true for i, v := range args { if v == "-no-color" { m.Color = false - return append(args[:i], args[i+1:]...) + args = append(args[:i], args[i+1:]...) + break } } + // Set the UI + m.oldUi = m.Ui + m.Ui = &ColorizeUi{ + Colorize: m.Colorize(), + ErrorColor: "[red]", + Ui: m.oldUi, + } + return args } From 9b090a5505edfdf1a08ef2f6534ae0c2e560bfdd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 12 Jul 2014 21:02:53 -0700 Subject: [PATCH 27/36] command/apply: use new state formatting --- command/apply.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/apply.go b/command/apply.go index d87e1954e..4922db4fd 100644 --- a/command/apply.go +++ b/command/apply.go @@ -120,7 +120,7 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } - c.Ui.Output(strings.TrimSpace(state.String())) + c.Ui.Output(FormatState(state, c.Colorize())) return 0 } From 4bc00fa047c91f270b14b173e94ca192d446da3a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 09:20:27 -0700 Subject: [PATCH 28/36] command/apply: cleaner output --- command/apply.go | 10 +++++++++- command/format_state.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/command/apply.go b/command/apply.go index 4922db4fd..1864d0593 100644 --- a/command/apply.go +++ b/command/apply.go @@ -120,7 +120,15 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } - c.Ui.Output(FormatState(state, c.Colorize())) + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + "[reset][bold][green]\n"+ + "Apply succeeded! Infrastructure created and/or updated.\n"+ + "The state of your infrastructure has been saved to the path\n"+ + "below. This state is required to modify and destroy your\n"+ + "infrastructure, so keep it safe. To inspect the complete state\n"+ + "use the `terraform show` command.\n\n"+ + "State path: %s", + stateOutPath))) return 0 } diff --git a/command/format_state.go b/command/format_state.go index 59f02e590..e05eadb50 100644 --- a/command/format_state.go +++ b/command/format_state.go @@ -78,5 +78,5 @@ func FormatState(s *terraform.State, c *colorstring.Colorize) string { } } - return strings.TrimSpace(buf.String()) + return strings.TrimSpace(c.Color(buf.String())) } From 687c5cd35c911e8a02a6ebd282513cd6083077a4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 09:21:44 -0700 Subject: [PATCH 29/36] command/show: remove unused fields in struct --- command/show.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/command/show.go b/command/show.go index c70e2b2ff..6f7a895b8 100644 --- a/command/show.go +++ b/command/show.go @@ -7,16 +7,12 @@ import ( "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 { Meta - - ContextOpts *terraform.ContextOpts - Ui cli.Ui } func (c *ShowCommand) Run(args []string) int { From 2bf6f380dc7c7ffc5699422f8217fdd6c146817c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 09:22:23 -0700 Subject: [PATCH 30/36] command/show: fix tests --- command/show_test.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/command/show_test.go b/command/show_test.go index b33f630e9..c1bebf247 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -11,8 +11,10 @@ import ( func TestShow(t *testing.T) { ui := new(cli.MockUi) c := &ShowCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ @@ -27,8 +29,10 @@ func TestShow(t *testing.T) { func TestShow_noArgs(t *testing.T) { ui := new(cli.MockUi) c := &ShowCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{} @@ -44,8 +48,10 @@ func TestShow_plan(t *testing.T) { ui := new(cli.MockUi) c := &ShowCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ @@ -70,8 +76,10 @@ func TestShow_state(t *testing.T) { ui := new(cli.MockUi) c := &ShowCommand{ - ContextOpts: testCtxConfig(testProvider()), - Ui: ui, + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, } args := []string{ From 00ebedb4fb588edc1d383558c1a45dea34d005e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 09:23:59 -0700 Subject: [PATCH 31/36] command: better whitespace in state formatting --- command/format_state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/format_state.go b/command/format_state.go index e05eadb50..4a67ef7ee 100644 --- a/command/format_state.go +++ b/command/format_state.go @@ -78,5 +78,5 @@ func FormatState(s *terraform.State, c *colorstring.Colorize) string { } } - return strings.TrimSpace(c.Color(buf.String())) + return c.Color(strings.TrimSpace(buf.String())) } From 52d29a6ecf9014b397b7845185a437727afa0736 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 09:34:35 -0700 Subject: [PATCH 32/36] command/apply: outputs --- command/apply.go | 32 ++++++++++++++++++++++++++++++++ command/hook_ui.go | 9 +++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/command/apply.go b/command/apply.go index 1864d0593..0276d9847 100644 --- a/command/apply.go +++ b/command/apply.go @@ -1,9 +1,11 @@ package command import ( + "bytes" "flag" "fmt" "os" + "sort" "strings" "github.com/hashicorp/terraform/terraform" @@ -130,6 +132,36 @@ func (c *ApplyCommand) Run(args []string) int { "State path: %s", stateOutPath))) + // If we have outputs, then output those at the end. + if len(state.Outputs) > 0 { + outputBuf := new(bytes.Buffer) + outputBuf.WriteString("[reset][bold][green]\nOutputs:\n\n") + + // Output the outputs in alphabetical order + keyLen := 0 + keys := make([]string, 0, len(state.Outputs)) + for key, _ := range state.Outputs { + keys = append(keys, key) + if len(key) > keyLen { + keyLen = len(key) + } + } + sort.Strings(keys) + + for _, k := range keys { + v := state.Outputs[k] + + outputBuf.WriteString(fmt.Sprintf( + " %s%s = %s\n", + k, + strings.Repeat(" ", keyLen-len(k)), + v)) + } + + c.Ui.Output(c.Colorize().Color( + strings.TrimSpace(outputBuf.String()))) + } + return 0 } diff --git a/command/hook_ui.go b/command/hook_ui.go index 3f4d9bf5f..eba23c6e3 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -71,11 +71,16 @@ func (h *UiHook) PreApply( v)) } + attrString := strings.TrimSpace(attrBuf.String()) + if attrString != "" { + attrString = "\n " + attrString + } + h.ui.Output(h.Colorize.Color(fmt.Sprintf( - "[reset][bold]%s: %s[reset_bold]\n %s", + "[reset][bold]%s: %s[reset_bold]%s", id, operation, - strings.TrimSpace(attrBuf.String())))) + attrString))) return terraform.HookActionContinue, nil } From 2caff709d66a8af421e1bd9e9d44dfd5a4383c54 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 10:25:42 -0700 Subject: [PATCH 33/36] comand/output --- command/output.go | 92 +++++++++++++++++++++ command/output_test.go | 182 +++++++++++++++++++++++++++++++++++++++++ commands.go | 6 ++ 3 files changed, 280 insertions(+) create mode 100644 command/output.go create mode 100644 command/output_test.go diff --git a/command/output.go b/command/output.go new file mode 100644 index 000000000..7e5ec1776 --- /dev/null +++ b/command/output.go @@ -0,0 +1,92 @@ +package command + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/terraform" +) + +// OutputCommand is a Command implementation that reads an output +// from a Terraform state and prints it. +type OutputCommand struct { + Meta +} + +func (c *OutputCommand) Run(args []string) int { + var statePath string + + args = c.Meta.process(args) + + cmdFlags := flag.NewFlagSet("output", flag.ContinueOnError) + cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") + 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 output command expects exactly one argument with the name\n" + + "of an output variable.\n") + cmdFlags.Usage() + return 1 + } + name := args[0] + + f, err := os.Open(statePath) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error loading file: %s", err)) + return 1 + } + + state, err := terraform.ReadState(f) + f.Close() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) + return 1 + } + + if len(state.Outputs) == 0 { + c.Ui.Error(fmt.Sprintf( + "The state file has no outputs defined. Define an output\n" + + "in your configuration with the `output` directive and re-run\n" + + "`terraform apply` for it to become available.")) + return 1 + } + v, ok := state.Outputs[name] + if !ok { + c.Ui.Error(fmt.Sprintf( + "The output variable requested could not be found in the state\n" + + "file. If you recently added this to your configuration, be\n" + + "sure to run `terraform apply`, since the state won't be updated\n" + + "with new output variables until that command is run.")) + return 1 + } + + c.Ui.Output(v) + return 0 +} + +func (c *OutputCommand) Help() string { + helpText := ` +Usage: terraform output [options] NAME + + Reads an output variable from a Terraform state file and prints + the value. + +Options: + + -state=path Path to the state file to read. Defaults to + "terraform.tfstate". + +` + return strings.TrimSpace(helpText) +} + +func (c *OutputCommand) Synopsis() string { + return "Read an output from a state file" +} diff --git a/command/output_test.go b/command/output_test.go new file mode 100644 index 000000000..3c4a29e20 --- /dev/null +++ b/command/output_test.go @@ -0,0 +1,182 @@ +package command + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func TestOutput(t *testing.T) { + originalState := &terraform.State{ + Outputs: map[string]string{ + "foo": "bar", + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + if actual != "bar" { + t.Fatalf("bad: %#v", actual) + } +} + +func TestOutput_badVar(t *testing.T) { + originalState := &terraform.State{ + Outputs: map[string]string{ + "foo": "bar", + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "bar", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestOutput_manyArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + 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 TestOutput_noArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + } +} + +func TestOutput_noVars(t *testing.T) { + originalState := &terraform.State{ + Outputs: map[string]string{}, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "bar", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestOutput_stateDefault(t *testing.T) { + originalState := &terraform.State{ + Outputs: map[string]string{ + "foo": "bar", + }, + } + + // Write the state file in a temporary directory with the + // default filename. + td, err := ioutil.TempDir("", "tf") + if err != nil { + t.Fatalf("err: %s", err) + } + statePath := filepath.Join(td, DefaultStateFilename) + + f, err := os.Create(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + err = terraform.WriteState(originalState, f) + f.Close() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Change to that directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(filepath.Dir(statePath)); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + actual := strings.TrimSpace(ui.OutputWriter.String()) + if actual != "bar" { + t.Fatalf("bad: %#v", actual) + } +} diff --git a/commands.go b/commands.go index ee0dc69ba..e7e4d052d 100644 --- a/commands.go +++ b/commands.go @@ -45,6 +45,12 @@ func init() { }, nil }, + "output": func() (cli.Command, error) { + return &command.OutputCommand{ + Meta: meta, + }, nil + }, + "plan": func() (cli.Command, error) { return &command.PlanCommand{ Meta: meta, From 3f803cb75c70a7089bda2dd170d28010e75cedd3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 10:29:31 -0700 Subject: [PATCH 34/36] command/output: protect againts blank params --- command/output.go | 2 +- command/output_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/command/output.go b/command/output.go index 7e5ec1776..6c9d94b8e 100644 --- a/command/output.go +++ b/command/output.go @@ -28,7 +28,7 @@ func (c *OutputCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) != 1 { + if len(args) != 1 || args[0] == "" { c.Ui.Error( "The output command expects exactly one argument with the name\n" + "of an output variable.\n") diff --git a/command/output_test.go b/command/output_test.go index 3c4a29e20..f177198c5 100644 --- a/command/output_test.go +++ b/command/output_test.go @@ -68,6 +68,32 @@ func TestOutput_badVar(t *testing.T) { } } +func TestOutput_blank(t *testing.T) { + originalState := &terraform.State{ + Outputs: map[string]string{ + "foo": "bar", + }, + } + + statePath := testStateFile(t, originalState) + + ui := new(cli.MockUi) + c := &OutputCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "", + } + if code := c.Run(args); code != 1 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + func TestOutput_manyArgs(t *testing.T) { ui := new(cli.MockUi) c := &OutputCommand{ From 3a8606c1170e9ecc738aefcf9033da84f7a3583b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 10:37:25 -0700 Subject: [PATCH 35/36] Recognize -version --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 49331e192..2fa5b1760 100644 --- a/main.go +++ b/main.go @@ -91,7 +91,7 @@ func wrappedMain() int { // just show the version. args := os.Args[1:] for _, arg := range args { - if arg == "-v" || arg == "--version" { + if arg == "-v" || arg == "-version" || arg == "--version" { newArgs := make([]string, len(args)+1) newArgs[0] = "version" copy(newArgs[1:], args) From dde0f0f8df3845b7d867ae9e0a61d6cdef3d6c77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Jul 2014 10:42:18 -0700 Subject: [PATCH 36/36] command/version: use Meta --- command/version.go | 10 ++++++---- commands.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/command/version.go b/command/version.go index 4e433435f..999af7abf 100644 --- a/command/version.go +++ b/command/version.go @@ -3,24 +3,26 @@ package command import ( "bytes" "fmt" - - "github.com/mitchellh/cli" ) // VersionCommand is a Command implementation prints the version. type VersionCommand struct { + Meta + Revision string Version string VersionPrerelease string - Ui cli.Ui } func (c *VersionCommand) Help() string { return "" } -func (c *VersionCommand) Run(_ []string) int { +func (c *VersionCommand) Run(args []string) int { var versionString bytes.Buffer + + args = c.Meta.process(args) + fmt.Fprintf(&versionString, "Terraform v%s", c.Version) if c.VersionPrerelease != "" { fmt.Fprintf(&versionString, ".%s", c.VersionPrerelease) diff --git a/commands.go b/commands.go index e7e4d052d..870ca39d1 100644 --- a/commands.go +++ b/commands.go @@ -71,10 +71,10 @@ func init() { "version": func() (cli.Command, error) { return &command.VersionCommand{ + Meta: meta, Revision: GitCommit, Version: Version, VersionPrerelease: VersionPrerelease, - Ui: Ui, }, nil }, }