diff --git a/command/env_command.go b/command/env_command.go index 9ad03b5eb..d2f16e8e1 100644 --- a/command/env_command.go +++ b/command/env_command.go @@ -1,13 +1,10 @@ package command import ( - "bytes" "fmt" - "os" "strings" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -15,33 +12,19 @@ import ( // environments. type EnvCommand struct { Meta - - newEnv string - delEnv string - statePath string - force bool - - // backend returns by Meta.Backend - b backend.Backend - // MultiState Backend - multi backend.MultiState } func (c *EnvCommand) Run(args []string) int { args = c.Meta.process(args, true) cmdFlags := c.Meta.flagSet("env") - cmdFlags.StringVar(&c.newEnv, "new", "", "create a new environment") - cmdFlags.StringVar(&c.delEnv, "delete", "", "delete an existing environment") - cmdFlags.StringVar(&c.statePath, "state", "", "terraform state file") - cmdFlags.BoolVar(&c.force, "force", false, "force removal of a non-empty environment") 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("0 or 1 arguments expected.\n") + if len(args) > 0 { + c.Ui.Error("0 arguments expected.\n") return cli.RunResultHelp } @@ -51,240 +34,36 @@ func (c *EnvCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) return 1 } - c.b = b multi, ok := b.(backend.MultiState) if !ok { c.Ui.Error(envNotSupported) return 1 } - c.multi = multi - - if c.newEnv != "" { - return c.createEnv() - } - - if c.delEnv != "" { - return c.deleteEnv() - } - - if len(args) == 1 { - return c.changeEnv(args[0]) - } - - return c.listEnvs() -} - -func (c *EnvCommand) createEnv() int { - states, _, err := c.multi.States() - for _, s := range states { - if c.newEnv == s { - c.Ui.Error(fmt.Sprintf(envExists, c.newEnv)) - return 1 - } - } - - err = c.multi.ChangeState(c.newEnv) + _, current, err := multi.States() if err != nil { c.Ui.Error(err.Error()) return 1 } - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envCreated, c.newEnv), - ), - ) - - if c.statePath == "" { - // if we're not loading a state, then we're done - return 0 - } - - // load the new state - sMgr, err := c.b.State() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // load the existing state - stateFile, err := os.Open(c.statePath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - s, err := terraform.ReadState(stateFile) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - err = sMgr.WriteState(s) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - return 0 -} - -func (c *EnvCommand) deleteEnv() int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - exists := false - for _, s := range states { - if c.delEnv == s { - exists = true - break - } - } - - if !exists { - c.Ui.Error(fmt.Sprintf(envDoesNotExist, c.delEnv)) - return 1 - } - - // In order to check if the state being deleted is empty, we need to change - // to that state and load it. - if current != c.delEnv { - if err := c.multi.ChangeState(c.delEnv); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - // always try to change back after - defer func() { - if err := c.multi.ChangeState(current); err != nil { - c.Ui.Error(err.Error()) - } - }() - } - - // we need the actual state to see if it's empty - sMgr, err := c.b.State() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - if err := sMgr.RefreshState(); err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - empty := sMgr.State().Empty() - - if !empty && !c.force { - c.Ui.Error(fmt.Sprintf(envNotEmpty, c.delEnv)) - return 1 - } - - err = c.multi.DeleteState(c.delEnv) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envDeleted, c.delEnv), - ), - ) - - if !empty { - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envWarnNotEmpty, c.delEnv), - ), - ) - } - - return 0 -} - -func (c *EnvCommand) changeEnv(name string) int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - if current == name { - return 0 - } - - found := false - for _, s := range states { - if name == s { - found = true - break - } - } - - if !found { - c.Ui.Error(fmt.Sprintf(envDoesNotExist, name)) - return 1 - } - - err = c.multi.ChangeState(name) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envChanged, name), - ), - ) - - return 0 -} - -func (c *EnvCommand) listEnvs() int { - states, current, err := c.multi.States() - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - var out bytes.Buffer - for _, s := range states { - if s == current { - out.WriteString("* ") - } else { - out.WriteString(" ") - } - out.WriteString(s + "\n") - } - - c.Ui.Output(out.String()) + c.Ui.Output(fmt.Sprintf("Current environment is %q\n", current)) + c.Ui.Output(c.Help()) return 0 } func (c *EnvCommand) Help() string { helpText := ` -Usage: terraform env [options] [NAME] +Usage: terraform env Create, change and delete Terraform environments. - By default env will list all configured environments. If NAME is provided, - env will change into to that named environment. +Subcommands: -Options: - - -new=name Create a new environment. - -delete=name Delete an existing environment, - - -state=path Used with -new to copy a state file into the new environment. - -force Used with -delete to remove a non-empty environment. + list List environments. + select Select an environment. + new Create a new environment. + delete Delete an existing environment. ` return strings.TrimSpace(helpText) } diff --git a/command/env_command_test.go b/command/env_command_test.go index 8257932ad..7caa11932 100644 --- a/command/env_command_test.go +++ b/command/env_command_test.go @@ -22,7 +22,7 @@ func TestEnv_createAndChange(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - c := &EnvCommand{} + newCmd := &EnvNewCommand{} current, err := currentEnv() if err != nil { @@ -32,10 +32,10 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatal("current env should be 'default'") } - args := []string{"-new", "test"} + args := []string{"test"} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + newCmd.Meta = Meta{Ui: ui} + if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -47,10 +47,11 @@ func TestEnv_createAndChange(t *testing.T) { t.Fatal("current env should be 'test'") } + selCmd := &EnvSelectCommand{} args = []string{backend.DefaultStateName} ui = new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + selCmd.Meta = Meta{Ui: ui} + if code := selCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -74,31 +75,30 @@ func TestEnv_createAndList(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - c := &EnvCommand{} + newCmd := &EnvNewCommand{} envs := []string{"test_a", "test_b", "test_c"} // create multiple envs for _, env := range envs { - args := []string{"-new", env} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} - if code := c.Run(args); code != 0 { + newCmd.Meta = Meta{Ui: ui} + if code := newCmd.Run([]string{env}); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } } - // now check the listing - expected := "default\n test_a\n test_b\n* test_c" - + listCmd := &EnvListCommand{} ui := new(cli.MockUi) - c.Meta = Meta{Ui: ui} + listCmd.Meta = Meta{Ui: ui} - if code := c.Run(nil); code != 0 { + if code := listCmd.Run(nil); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } actual := strings.TrimSpace(ui.OutputWriter.String()) + expected := "default\n test_a\n test_b\n* test_c" + if actual != expected { t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual) } @@ -132,12 +132,12 @@ func TestEnv_createWithState(t *testing.T) { t.Fatal(err) } - args := []string{"-new", "test", "-state", "test.tfstate"} + args := []string{"-state", "test.tfstate", "test"} ui := new(cli.MockUi) - c := &EnvCommand{ + newCmd := &EnvNewCommand{ Meta: Meta{Ui: ui}, } - if code := c.Run(args); code != 0 { + if code := newCmd.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) } @@ -183,11 +183,11 @@ func TestEnv_delete(t *testing.T) { } ui := new(cli.MockUi) - c := &EnvCommand{ + delCmd := &EnvDeleteCommand{ Meta: Meta{Ui: ui}, } - args := []string{"-delete", "test"} - if code := c.Run(args); code != 0 { + args := []string{"test"} + if code := delCmd.Run(args); code != 0 { t.Fatalf("failure: %s", ui.ErrorWriter) } @@ -235,19 +235,19 @@ func TestEnv_deleteWithState(t *testing.T) { } ui := new(cli.MockUi) - c := &EnvCommand{ + delCmd := &EnvDeleteCommand{ Meta: Meta{Ui: ui}, } - args := []string{"-delete", "test"} - if code := c.Run(args); code == 0 { + args := []string{"test"} + if code := delCmd.Run(args); code == 0 { t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter) } ui = new(cli.MockUi) - c.Meta.Ui = ui + delCmd.Meta.Ui = ui - args = []string{"-delete", "test", "-force"} - if code := c.Run(args); code != 0 { + args = []string{"-force", "test"} + if code := delCmd.Run(args); code != 0 { t.Fatalf("failure: %s", ui.ErrorWriter) } diff --git a/command/env_delete.go b/command/env_delete.go new file mode 100644 index 000000000..5295e4db4 --- /dev/null +++ b/command/env_delete.go @@ -0,0 +1,151 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" + "github.com/mitchellh/cli" + + clistate "github.com/hashicorp/terraform/command/state" +) + +type EnvDeleteCommand struct { + Meta +} + +func (c *EnvDeleteCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + force := false + cmdFlags := c.Meta.flagSet("env") + cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty environment") + 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("expected NAME.\n") + return cli.RunResultHelp + } + + delEnv := args[0] + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + exists := false + for _, s := range states { + if delEnv == s { + exists = true + break + } + } + + if !exists { + c.Ui.Error(fmt.Sprintf(envDoesNotExist, delEnv)) + return 1 + } + + // In order to check if the state being deleted is empty, we need to change + // to that state and load it. + if current != delEnv { + if err := multi.ChangeState(delEnv); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // always try to change back after + defer func() { + if err := multi.ChangeState(current); err != nil { + c.Ui.Error(err.Error()) + } + }() + } + + // we need the actual state to see if it's empty + sMgr, err := b.State() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if err := sMgr.RefreshState(); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + empty := sMgr.State().Empty() + + if !empty && !force { + c.Ui.Error(fmt.Sprintf(envNotEmpty, delEnv)) + return 1 + } + + // Lock the state if we can + lockInfo := state.NewLockInfo() + lockInfo.Operation = "env new" + lockID, err := clistate.Lock(sMgr, lockInfo, c.Ui, c.Colorize()) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + return 1 + } + defer clistate.Unlock(sMgr, lockID, c.Ui, c.Colorize()) + + err = multi.DeleteState(delEnv) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envDeleted, delEnv), + ), + ) + + if !empty { + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envWarnNotEmpty, delEnv), + ), + ) + } + + return 0 +} +func (c *EnvDeleteCommand) Help() string { + helpText := ` +Usage: terraform env delete [OPTIONS] NAME + + Delete a Terraform environment + + +Options: + + -force remove a non-empty environment. +` + return strings.TrimSpace(helpText) +} + +func (c *EnvDeleteCommand) Synopsis() string { + return "Delete an environment" +} diff --git a/command/env_list.go b/command/env_list.go new file mode 100644 index 000000000..23761b99e --- /dev/null +++ b/command/env_list.go @@ -0,0 +1,68 @@ +package command + +import ( + "bytes" + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" +) + +type EnvListCommand struct { + Meta +} + +func (c *EnvListCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + cmdFlags := c.Meta.flagSet("env list") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + var out bytes.Buffer + for _, s := range states { + if s == current { + out.WriteString("* ") + } else { + out.WriteString(" ") + } + out.WriteString(s + "\n") + } + + c.Ui.Output(out.String()) + return 0 +} + +func (c *EnvListCommand) Help() string { + helpText := ` +Usage: terraform env list + + List Terraform environments. +` + return strings.TrimSpace(helpText) +} + +func (c *EnvListCommand) Synopsis() string { + return "List Environments" +} diff --git a/command/env_new.go b/command/env_new.go new file mode 100644 index 000000000..026199eeb --- /dev/null +++ b/command/env_new.go @@ -0,0 +1,133 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" + + clistate "github.com/hashicorp/terraform/command/state" +) + +type EnvNewCommand struct { + Meta +} + +func (c *EnvNewCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + statePath := "" + + cmdFlags := c.Meta.flagSet("env new") + cmdFlags.StringVar(&statePath, "state", "", "terraform state file") + 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("expected NAME.\n") + return cli.RunResultHelp + } + + newEnv := args[0] + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, _, err := multi.States() + for _, s := range states { + if newEnv == s { + c.Ui.Error(fmt.Sprintf(envExists, newEnv)) + return 1 + } + } + + err = multi.ChangeState(newEnv) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envCreated, newEnv), + ), + ) + + if statePath == "" { + // if we're not loading a state, then we're done + return 0 + } + + // load the new Backend state + sMgr, err := b.State() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // Lock the state if we can + lockInfo := state.NewLockInfo() + lockInfo.Operation = "env new" + lockID, err := clistate.Lock(sMgr, lockInfo, c.Ui, c.Colorize()) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) + return 1 + } + defer clistate.Unlock(sMgr, lockID, c.Ui, c.Colorize()) + + // read the existing state file + stateFile, err := os.Open(statePath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + s, err := terraform.ReadState(stateFile) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // save the existing state in the new Backend. + err = sMgr.WriteState(s) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + return 0 +} + +func (c *EnvNewCommand) Help() string { + helpText := ` +Usage: terraform env new [OPTIONS] NAME + + Create a new Terraform environment. + + +Options: + + -state=path Copy an existing state file into the new environment. +` + return strings.TrimSpace(helpText) +} + +func (c *EnvNewCommand) Synopsis() string { + return "Create a new environment" +} diff --git a/command/env_select.go b/command/env_select.go new file mode 100644 index 000000000..7598b4204 --- /dev/null +++ b/command/env_select.go @@ -0,0 +1,93 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/backend" + "github.com/mitchellh/cli" +) + +type EnvSelectCommand struct { + Meta +} + +func (c *EnvSelectCommand) Run(args []string) int { + args = c.Meta.process(args, true) + + cmdFlags := c.Meta.flagSet("env select") + 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("expected NAME.\n") + return cli.RunResultHelp + } + + // Load the backend + b, err := c.Backend(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + return 1 + } + + name := args[0] + + multi, ok := b.(backend.MultiState) + if !ok { + c.Ui.Error(envNotSupported) + return 1 + } + + states, current, err := multi.States() + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + if current == name { + return 0 + } + + found := false + for _, s := range states { + if name == s { + found = true + break + } + } + + if !found { + c.Ui.Error(fmt.Sprintf(envDoesNotExist, name)) + return 1 + } + + err = multi.ChangeState(name) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envChanged, name), + ), + ) + + return 0 +} + +func (c *EnvSelectCommand) Help() string { + helpText := ` +Usage: terraform env select NAME + + Change Terraform environment. +` + return strings.TrimSpace(helpText) +} + +func (c *EnvSelectCommand) Synopsis() string { + return "Change environments" +} diff --git a/commands.go b/commands.go index 18ccd6d52..f2f7b4eda 100644 --- a/commands.go +++ b/commands.go @@ -75,6 +75,30 @@ func init() { }, nil }, + "env list": func() (cli.Command, error) { + return &command.EnvListCommand{ + Meta: meta, + }, nil + }, + + "env select": func() (cli.Command, error) { + return &command.EnvSelectCommand{ + Meta: meta, + }, nil + }, + + "env new": func() (cli.Command, error) { + return &command.EnvNewCommand{ + Meta: meta, + }, nil + }, + + "env delete": func() (cli.Command, error) { + return &command.EnvDeleteCommand{ + Meta: meta, + }, nil + }, + "fmt": func() (cli.Command, error) { return &command.FmtCommand{ Meta: meta,