Add basic env commands
Used a single command with flags for now. We may refactor this out to subcommands.
This commit is contained in:
parent
e6eb71dde5
commit
31f033827f
|
@ -0,0 +1,322 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// EnvCommand is a Command Implementation that manipulates local state
|
||||
// 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")
|
||||
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
|
||||
}
|
||||
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)
|
||||
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())
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform env [options] [NAME]
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *EnvCommand) Synopsis() string {
|
||||
return "Environment management"
|
||||
}
|
||||
|
||||
const (
|
||||
envNotSupported = `Backend does not support environments`
|
||||
|
||||
envExists = `Environment %q already exists`
|
||||
|
||||
envDoesNotExist = `Environment %q doesn't exist!
|
||||
You can create this environment with the "-new" option.`
|
||||
|
||||
envChanged = `[reset][green]Switched to environment %q!`
|
||||
|
||||
envCreated = `[reset][green]Created environment %q!`
|
||||
|
||||
envDeleted = `[reset][green]Deleted environment %q!`
|
||||
|
||||
envNotEmpty = `Environment %[1]q is not empty!
|
||||
Deleting %[1]q can result in dangling resources: resources that
|
||||
exist but are no longer manageable by Terraform. Please destroy
|
||||
these resources first. If you want to delete this environment
|
||||
anyways and risk dangling resources, use the '-force' flag.
|
||||
`
|
||||
|
||||
envWarnNotEmpty = `[reset][yellow]WARNING: %q was non-empty.
|
||||
The resources managed by the deleted environment may still exist,
|
||||
but are no longer manageable by Terraform since the state has
|
||||
been deleted.
|
||||
`
|
||||
)
|
|
@ -0,0 +1,312 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/backend/local"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestEnv_createAndChange(t *testing.T) {
|
||||
// Create a temporary working directory that is empty
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
c := &EnvCommand{}
|
||||
|
||||
current, err := currentEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatal("current env should be 'default'")
|
||||
}
|
||||
|
||||
args := []string{"-new", "test"}
|
||||
ui := new(cli.MockUi)
|
||||
c.Meta = Meta{Ui: ui}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current, err = currentEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != "test" {
|
||||
t.Fatal("current env should be 'test'")
|
||||
}
|
||||
|
||||
args = []string{backend.DefaultStateName}
|
||||
ui = new(cli.MockUi)
|
||||
c.Meta = Meta{Ui: ui}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current, err = currentEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatal("current env should be 'default'")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create some environments and test the list output.
|
||||
// This also ensures we switch to the correct env after each call
|
||||
func TestEnv_createAndList(t *testing.T) {
|
||||
// Create a temporary working directory that is empty
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
c := &EnvCommand{}
|
||||
|
||||
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 {
|
||||
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"
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c.Meta = Meta{Ui: ui}
|
||||
|
||||
if code := c.Run(nil); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(ui.OutputWriter.String())
|
||||
if actual != expected {
|
||||
t.Fatalf("\nexpcted: %q\nactual: %q", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv_createWithState(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create a non-empty state
|
||||
originalState := &terraform.State{
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := (&state.LocalState{Path: "test.tfstate"}).WriteState(originalState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
args := []string{"-new", "test", "-state", "test.tfstate"}
|
||||
ui := new(cli.MockUi)
|
||||
c := &EnvCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
|
||||
}
|
||||
|
||||
newPath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
|
||||
envState := state.LocalState{Path: newPath}
|
||||
err = envState.RefreshState()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newState := envState.State()
|
||||
if !originalState.Equal(newState) {
|
||||
t.Fatalf("states not equal\norig: %s\nnew: %s", originalState, newState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv_delete(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create the env directories
|
||||
if err := os.MkdirAll(filepath.Join(local.DefaultEnvDir, "test"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create the environment file
|
||||
if err := os.MkdirAll(DefaultDataDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(DefaultDataDir, local.DefaultEnvFile), []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
current, err := currentEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if current != "test" {
|
||||
t.Fatal("wrong env:", current)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &EnvCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
args := []string{"-delete", "test"}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("failure: %s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
current, err = currentEnv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if current != backend.DefaultStateName {
|
||||
t.Fatalf("wrong env: %q", current)
|
||||
}
|
||||
}
|
||||
func TestEnv_deleteWithState(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
os.MkdirAll(td, 0755)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// create the env directories
|
||||
if err := os.MkdirAll(filepath.Join(local.DefaultEnvDir, "test"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create a non-empty state
|
||||
originalState := &terraform.State{
|
||||
Modules: []*terraform.ModuleState{
|
||||
&terraform.ModuleState{
|
||||
Path: []string{"root"},
|
||||
Resources: map[string]*terraform.ResourceState{
|
||||
"test_instance.foo": &terraform.ResourceState{
|
||||
Type: "test_instance",
|
||||
Primary: &terraform.InstanceState{
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
envStatePath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
|
||||
err := (&state.LocalState{Path: envStatePath}).WriteState(originalState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &EnvCommand{
|
||||
Meta: Meta{Ui: ui},
|
||||
}
|
||||
args := []string{"-delete", "test"}
|
||||
if code := c.Run(args); code == 0 {
|
||||
t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter)
|
||||
}
|
||||
|
||||
ui = new(cli.MockUi)
|
||||
c.Meta.Ui = ui
|
||||
|
||||
args = []string{"-delete", "test", "-force"}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("failure: %s", ui.ErrorWriter)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(local.DefaultEnvDir, "test")); !os.IsNotExist(err) {
|
||||
t.Fatal("env 'test' still exists!")
|
||||
}
|
||||
}
|
||||
|
||||
func currentEnv() (string, error) {
|
||||
contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, local.DefaultEnvFile))
|
||||
if os.IsNotExist(err) {
|
||||
return backend.DefaultStateName, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
current := strings.TrimSpace(string(contents))
|
||||
if current == "" {
|
||||
current = backend.DefaultStateName
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func envStatePath() (string, error) {
|
||||
currentEnv, err := currentEnv()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if currentEnv == backend.DefaultStateName {
|
||||
return DefaultStateFilename, nil
|
||||
}
|
||||
|
||||
return filepath.Join(local.DefaultEnvDir, currentEnv, DefaultStateFilename), nil
|
||||
}
|
||||
|
||||
func listEnvs() ([]string, error) {
|
||||
entries, err := ioutil.ReadDir(local.DefaultEnvDir)
|
||||
// no error if there's no envs configured
|
||||
if os.IsNotExist(err) {
|
||||
return []string{backend.DefaultStateName}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var envs []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
envs = append(envs, filepath.Base(entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(envs)
|
||||
|
||||
// always start with "default"
|
||||
envs = append([]string{backend.DefaultStateName}, envs...)
|
||||
|
||||
return envs, nil
|
||||
}
|
|
@ -69,6 +69,12 @@ func init() {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"env": func() (cli.Command, error) {
|
||||
return &command.EnvCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"fmt": func() (cli.Command, error) {
|
||||
return &command.FmtCommand{
|
||||
Meta: meta,
|
||||
|
|
Loading…
Reference in New Issue