split the env command into subcommands

This commit is contained in:
James Bardin 2017-02-23 13:13:28 -05:00
parent 31f033827f
commit c8526484b3
7 changed files with 507 additions and 259 deletions

View File

@ -1,13 +1,10 @@
package command package command
import ( import (
"bytes"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -15,33 +12,19 @@ import (
// environments. // environments.
type EnvCommand struct { type EnvCommand struct {
Meta 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 { func (c *EnvCommand) Run(args []string) int {
args = c.Meta.process(args, true) args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("env") 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()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil { if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
} }
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) > 1 { if len(args) > 0 {
c.Ui.Error("0 or 1 arguments expected.\n") c.Ui.Error("0 arguments expected.\n")
return cli.RunResultHelp 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)) c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1 return 1
} }
c.b = b
multi, ok := b.(backend.MultiState) multi, ok := b.(backend.MultiState)
if !ok { if !ok {
c.Ui.Error(envNotSupported) c.Ui.Error(envNotSupported)
return 1 return 1
} }
c.multi = multi _, current, err := multi.States()
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)
if err != nil { if err != nil {
c.Ui.Error(err.Error()) c.Ui.Error(err.Error())
return 1 return 1
} }
c.Ui.Output( c.Ui.Output(fmt.Sprintf("Current environment is %q\n", current))
c.Colorize().Color( c.Ui.Output(c.Help())
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())
return 0 return 0
} }
func (c *EnvCommand) Help() string { func (c *EnvCommand) Help() string {
helpText := ` helpText := `
Usage: terraform env [options] [NAME] Usage: terraform env
Create, change and delete Terraform environments. 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: list List environments.
select Select an environment.
-new=name Create a new environment. new Create a new environment.
-delete=name Delete an existing environment, delete 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.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }

View File

@ -22,7 +22,7 @@ func TestEnv_createAndChange(t *testing.T) {
defer os.RemoveAll(td) defer os.RemoveAll(td)
defer testChdir(t, td)() defer testChdir(t, td)()
c := &EnvCommand{} newCmd := &EnvNewCommand{}
current, err := currentEnv() current, err := currentEnv()
if err != nil { if err != nil {
@ -32,10 +32,10 @@ func TestEnv_createAndChange(t *testing.T) {
t.Fatal("current env should be 'default'") t.Fatal("current env should be 'default'")
} }
args := []string{"-new", "test"} args := []string{"test"}
ui := new(cli.MockUi) ui := new(cli.MockUi)
c.Meta = Meta{Ui: ui} newCmd.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) 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'") t.Fatal("current env should be 'test'")
} }
selCmd := &EnvSelectCommand{}
args = []string{backend.DefaultStateName} args = []string{backend.DefaultStateName}
ui = new(cli.MockUi) ui = new(cli.MockUi)
c.Meta = Meta{Ui: ui} selCmd.Meta = Meta{Ui: ui}
if code := c.Run(args); code != 0 { if code := selCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 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 os.RemoveAll(td)
defer testChdir(t, td)() defer testChdir(t, td)()
c := &EnvCommand{} newCmd := &EnvNewCommand{}
envs := []string{"test_a", "test_b", "test_c"} envs := []string{"test_a", "test_b", "test_c"}
// create multiple envs // create multiple envs
for _, env := range envs { for _, env := range envs {
args := []string{"-new", env}
ui := new(cli.MockUi) ui := new(cli.MockUi)
c.Meta = Meta{Ui: ui} newCmd.Meta = Meta{Ui: ui}
if code := c.Run(args); code != 0 { if code := newCmd.Run([]string{env}); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
} }
} }
// now check the listing listCmd := &EnvListCommand{}
expected := "default\n test_a\n test_b\n* test_c"
ui := new(cli.MockUi) 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) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
} }
actual := strings.TrimSpace(ui.OutputWriter.String()) actual := strings.TrimSpace(ui.OutputWriter.String())
expected := "default\n test_a\n test_b\n* test_c"
if actual != expected { if actual != expected {
t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual) t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual)
} }
@ -132,12 +132,12 @@ func TestEnv_createWithState(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
args := []string{"-new", "test", "-state", "test.tfstate"} args := []string{"-state", "test.tfstate", "test"}
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &EnvCommand{ newCmd := &EnvNewCommand{
Meta: Meta{Ui: ui}, 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) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
} }
@ -183,11 +183,11 @@ func TestEnv_delete(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &EnvCommand{ delCmd := &EnvDeleteCommand{
Meta: Meta{Ui: ui}, Meta: Meta{Ui: ui},
} }
args := []string{"-delete", "test"} args := []string{"test"}
if code := c.Run(args); code != 0 { if code := delCmd.Run(args); code != 0 {
t.Fatalf("failure: %s", ui.ErrorWriter) t.Fatalf("failure: %s", ui.ErrorWriter)
} }
@ -235,19 +235,19 @@ func TestEnv_deleteWithState(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &EnvCommand{ delCmd := &EnvDeleteCommand{
Meta: Meta{Ui: ui}, Meta: Meta{Ui: ui},
} }
args := []string{"-delete", "test"} args := []string{"test"}
if code := c.Run(args); code == 0 { if code := delCmd.Run(args); code == 0 {
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter) t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
} }
ui = new(cli.MockUi) ui = new(cli.MockUi)
c.Meta.Ui = ui delCmd.Meta.Ui = ui
args = []string{"-delete", "test", "-force"} args = []string{"-force", "test"}
if code := c.Run(args); code != 0 { if code := delCmd.Run(args); code != 0 {
t.Fatalf("failure: %s", ui.ErrorWriter) t.Fatalf("failure: %s", ui.ErrorWriter)
} }

151
command/env_delete.go Normal file
View File

@ -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"
}

68
command/env_list.go Normal file
View File

@ -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"
}

133
command/env_new.go Normal file
View File

@ -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"
}

93
command/env_select.go Normal file
View File

@ -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"
}

View File

@ -75,6 +75,30 @@ func init() {
}, nil }, 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) { "fmt": func() (cli.Command, error) {
return &command.FmtCommand{ return &command.FmtCommand{
Meta: meta, Meta: meta,