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
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)
}

View File

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

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