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,