Merge #14952: rename "state environments" to "workspaces"

This commit is contained in:
Martin Atkins 2017-06-09 16:38:15 -07:00 committed by GitHub
commit 698c22c924
54 changed files with 909 additions and 661 deletions

View File

@ -136,8 +136,9 @@ type Operation struct {
// The duration to retry obtaining a State lock.
StateLockTimeout time.Duration
// Environment is the named state that should be loaded from the Backend.
Environment string
// Workspace is the name of the workspace that this operation should run
// in, which controls which named state is used.
Workspace string
}
// RunningOperation is the result of starting an operation.

View File

@ -8,7 +8,6 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/hashicorp/terraform/backend"
@ -20,8 +19,8 @@ import (
)
const (
DefaultEnvDir = "terraform.tfstate.d"
DefaultEnvFile = "environment"
DefaultWorkspaceDir = "terraform.tfstate.d"
DefaultWorkspaceFile = "environment"
DefaultStateFilename = "terraform.tfstate"
DefaultDataDir = ".terraform"
DefaultBackupExtension = ".backup"
@ -36,8 +35,8 @@ type Local struct {
CLI cli.Ui
CLIColor *colorstring.Colorize
// The State* paths are set from the CLI options, and may be left blank to
// use the defaults. If the actual paths for the local backend state are
// The State* paths are set from the backend config, and may be left blank
// to use the defaults. If the actual paths for the local backend state are
// needed, use the StatePaths method.
//
// StatePath is the local path where state is read from.
@ -48,12 +47,12 @@ type Local struct {
// StateBackupPath is the local path where a backup file will be written.
// Set this to "-" to disable state backup.
//
// StateEnvPath is the path to the folder containing environments. This
// defaults to DefaultEnvDir if not set.
StatePath string
StateOutPath string
StateBackupPath string
StateEnvDir string
// StateWorkspaceDir is the path to the folder containing data for
// non-default workspaces. This defaults to DefaultWorkspaceDir if not set.
StatePath string
StateOutPath string
StateBackupPath string
StateWorkspaceDir string
// We only want to create a single instance of a local state, so store them
// here as they're loaded.
@ -127,7 +126,7 @@ func (b *Local) States() ([]string, error) {
// the listing always start with "default"
envs := []string{backend.DefaultStateName}
entries, err := ioutil.ReadDir(b.stateEnvDir())
entries, err := ioutil.ReadDir(b.stateWorkspaceDir())
// no error if there's no envs configured
if os.IsNotExist(err) {
return envs, nil
@ -166,7 +165,7 @@ func (b *Local) DeleteState(name string) error {
}
delete(b.states, name)
return os.RemoveAll(filepath.Join(b.stateEnvDir(), name))
return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name))
}
func (b *Local) State(name string) (state.State, error) {
@ -292,11 +291,20 @@ func (b *Local) init() {
Default: "",
},
"environment_dir": &schema.Schema{
"workspace_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
},
"environment_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "",
ConflictsWith: []string{"workspace_dir"},
Deprecated: "workspace_dir should be used instead, with the same meaning",
},
},
ConfigureFunc: b.schemaConfigure,
@ -318,10 +326,18 @@ func (b *Local) schemaConfigure(ctx context.Context) error {
b.StateOutPath = path
}
if raw, ok := d.GetOk("workspace_dir"); ok {
path := raw.(string)
if path != "" {
b.StateWorkspaceDir = path
}
}
// Legacy name, which ConflictsWith workspace_dir
if raw, ok := d.GetOk("environment_dir"); ok {
path := raw.(string)
if path != "" {
b.StateEnvDir = path
b.StateWorkspaceDir = path
}
}
@ -344,7 +360,7 @@ func (b *Local) StatePaths(name string) (string, string, string) {
statePath = DefaultStateFilename
}
} else {
statePath = filepath.Join(b.stateEnvDir(), name, DefaultStateFilename)
statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename)
}
if stateOutPath == "" {
@ -367,7 +383,7 @@ func (b *Local) createState(name string) error {
return nil
}
stateDir := filepath.Join(b.stateEnvDir(), name)
stateDir := filepath.Join(b.stateWorkspaceDir(), name)
s, err := os.Stat(stateDir)
if err == nil && s.IsDir() {
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
@ -383,30 +399,11 @@ func (b *Local) createState(name string) error {
return nil
}
// stateEnvDir returns the directory where state environments are stored.
func (b *Local) stateEnvDir() string {
if b.StateEnvDir != "" {
return b.StateEnvDir
// stateWorkspaceDir returns the directory where state environments are stored.
func (b *Local) stateWorkspaceDir() string {
if b.StateWorkspaceDir != "" {
return b.StateWorkspaceDir
}
return DefaultEnvDir
}
// currentStateName returns the name of the current named state as set in the
// configuration files.
// If there are no configured environments, currentStateName returns "default"
func (b *Local) currentStateName() (string, error) {
contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, DefaultEnvFile))
if os.IsNotExist(err) {
return backend.DefaultStateName, nil
}
if err != nil {
return "", err
}
if fromFile := strings.TrimSpace(string(contents)); fromFile != "" {
return fromFile, nil
}
return backend.DefaultStateName, nil
return DefaultWorkspaceDir
}

View File

@ -23,7 +23,7 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State,
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
// Get the state.
s, err := b.State(op.Environment)
s, err := b.State(op.Workspace)
if err != nil {
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
}

View File

@ -69,7 +69,7 @@ func TestLocal_StatePaths(t *testing.T) {
testEnv := "test_env"
path, out, back = b.StatePaths(testEnv)
expectedPath := filepath.Join(DefaultEnvDir, testEnv, DefaultStateFilename)
expectedPath := filepath.Join(DefaultWorkspaceDir, testEnv, DefaultStateFilename)
expectedOut := expectedPath
expectedBackup := expectedPath + DefaultBackupExtension
@ -261,7 +261,7 @@ func TestLocal_remoteStateBackup(t *testing.T) {
t.Fatal("remote state is not backed up")
}
if bs.Path != filepath.Join(DefaultEnvDir, "test", DefaultStateFilename+DefaultBackupExtension) {
if bs.Path != filepath.Join(DefaultWorkspaceDir, "test", DefaultStateFilename+DefaultBackupExtension) {
t.Fatal("bad backup location:", bs.Path)
}
}

View File

@ -18,11 +18,11 @@ import (
func TestLocal(t *testing.T) *Local {
tempDir := testTempDir(t)
return &Local{
StatePath: filepath.Join(tempDir, "state.tfstate"),
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
StateEnvDir: filepath.Join(tempDir, "state.tfstate.d"),
ContextOpts: &terraform.ContextOpts{},
StatePath: filepath.Join(tempDir, "state.tfstate"),
StateOutPath: filepath.Join(tempDir, "state.tfstate"),
StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"),
StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"),
ContextOpts: &terraform.ContextOpts{},
}
}

View File

@ -40,6 +40,14 @@ func dataSourceRemoteState() *schema.Resource {
"environment": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"workspace"},
Deprecated: "use the \"workspace\" argument instead, with the same value",
},
"workspace": {
Type: schema.TypeString,
Optional: true,
Default: backend.DefaultStateName,
},
@ -80,8 +88,12 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error {
}
// Get the state
env := d.Get("environment").(string)
state, err := b.State(env)
workspace := d.Get("environment").(string)
if workspace == "" {
// This is actually the main path, since "environment" is deprecated.
workspace = d.Get("workspace").(string)
}
state, err := b.State(workspace)
if err != nil {
return fmt.Errorf("error loading the remote state: %s", err)
}

View File

@ -63,6 +63,38 @@ func TestState_complexOutputs(t *testing.T) {
})
}
func TestState_workspace(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccState_workspace,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue(
"data.terraform_remote_state.foo", "foo", "bar"),
),
},
},
})
}
func TestState_legacyEnvironment(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccState_legacyEnvironment,
Check: resource.ComposeTestCheckFunc(
testAccCheckStateValue(
"data.terraform_remote_state.foo", "foo", "bar"),
),
},
},
})
}
func testAccCheckStateValue(id, name, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[id]
@ -109,3 +141,23 @@ resource "terraform_remote_state" "foo" {
path = "./test-fixtures/complex_outputs.tfstate"
}
}`
const testAccState_workspace = `
data "terraform_remote_state" "foo" {
backend = "local"
workspace = "test"
config {
workspace_dir = "./test-fixtures/workspaces"
}
}`
const testAccState_legacyEnvironment = `
data "terraform_remote_state" "foo" {
backend = "local"
environment = "test" # old, deprecated name for "workspace"
config {
workspace_dir = "./test-fixtures/workspaces"
}
}`

View File

@ -0,0 +1,7 @@
{
"version": 1,
"modules": [{
"path": ["root"],
"outputs": { "foo": "bar" }
}]
}

View File

@ -1639,7 +1639,7 @@ func TestApply_terraformEnvNonDefault(t *testing.T) {
// Create new env
{
ui := new(cli.MockUi)
newCmd := &EnvNewCommand{}
newCmd := &WorkspaceNewCommand{}
newCmd.Meta = Meta{Ui: ui}
if code := newCmd.Run([]string{"test"}); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
@ -1650,7 +1650,7 @@ func TestApply_terraformEnvNonDefault(t *testing.T) {
{
args := []string{"test"}
ui := new(cli.MockUi)
selCmd := &EnvSelectCommand{}
selCmd := &WorkspaceSelectCommand{}
selCmd.Meta = Meta{Ui: ui}
if code := selCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)

View File

@ -1,100 +0,0 @@
package command
import (
"net/url"
"strings"
)
// EnvCommand is a Command Implementation that manipulates local state
// environments.
type EnvCommand struct {
Meta
}
func (c *EnvCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("env")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
c.Ui.Output(c.Help())
return 0
}
func (c *EnvCommand) Help() string {
helpText := `
Usage: terraform env
Create, change and delete Terraform environments.
Subcommands:
list List environments.
select Select an environment.
new Create a new environment.
delete Delete an existing environment.
`
return strings.TrimSpace(helpText)
}
func (c *EnvCommand) Synopsis() string {
return "Environment management"
}
// validEnvName returns true is this name is valid to use as an environment name.
// Since most named states are accessed via a filesystem path or URL, check if
// escaping the name would be required.
func validEnvName(name string) bool {
return name == url.PathEscape(name)
}
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][bold]Created and switched to environment %q![reset][green]
You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
`
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.
`
envDelCurrent = `
Environment %[1]q is your active environment!
You cannot delete the currently active environment. Please switch
to another environment and try again.
`
envInvalidName = `
The environment name %q is not allowed. The name must contain only URL safe
characters, and no path separators.
`
)

View File

@ -177,7 +177,7 @@ func (c *InitCommand) Run(args []string) int {
// Now that we have loaded all modules, check the module tree for missing providers
if flagGetPlugins {
sMgr, err := back.State(c.Env())
sMgr, err := back.State(c.Workspace())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading state: %s", err))

View File

@ -247,7 +247,7 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
opts.ProviderSHA256s = m.providerPluginsLock().Read()
opts.Meta = &terraform.ContextMeta{
Env: m.Env(),
Env: m.Workspace(),
}
return &opts
@ -454,30 +454,51 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
return true
}
// Env returns the name of the currently configured environment, corresponding
// WorkspaceNameEnvVar is the name of the environment variable that can be used
// to set the name of the Terraform workspace, overriding the workspace chosen
// by `terraform workspace select`.
//
// Note that this environment variable is ignored by `terraform workspace new`
// and `terraform workspace delete`.
const WorkspaceNameEnvVar = "TF_WORKSPACE"
// Workspace returns the name of the currently configured workspace, corresponding
// to the desired named state.
func (m *Meta) Env() string {
func (m *Meta) Workspace() string {
current, _ := m.WorkspaceOverridden()
return current
}
// WorkspaceOverridden returns the name of the currently configured workspace,
// corresponding to the desired named state, as well as a bool saying whether
// this was set via the TF_WORKSPACE environment variable.
func (m *Meta) WorkspaceOverridden() (string, bool) {
if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
return envVar, true
}
dataDir := m.dataDir
if m.dataDir == "" {
dataDir = DefaultDataDir
}
envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultEnvFile))
envData, err := ioutil.ReadFile(filepath.Join(dataDir, local.DefaultWorkspaceFile))
current := string(bytes.TrimSpace(envData))
if current == "" {
current = backend.DefaultStateName
}
if err != nil && !os.IsNotExist(err) {
// always return the default if we can't get an environment name
log.Printf("[ERROR] failed to read current environment: %s", err)
// always return the default if we can't get a workspace name
log.Printf("[ERROR] failed to read current workspace: %s", err)
}
return current
return current, false
}
// SetEnv saves the named environment to the local filesystem.
func (m *Meta) SetEnv(name string) error {
// SetWorkspace saves the given name as the current workspace in the local
// filesystem.
func (m *Meta) SetWorkspace(name string) error {
dataDir := m.dataDir
if m.dataDir == "" {
dataDir = DefaultDataDir
@ -488,7 +509,7 @@ func (m *Meta) SetEnv(name string) error {
return err
}
err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultEnvFile), []byte(name), 0644)
err = ioutil.WriteFile(filepath.Join(dataDir, local.DefaultWorkspaceFile), []byte(name), 0644)
if err != nil {
return err
}

View File

@ -168,7 +168,7 @@ func (m *Meta) Operation() *backend.Operation {
PlanOutBackend: m.backendState,
Targets: m.targets,
UIIn: m.UIInput(),
Environment: m.Env(),
Workspace: m.Workspace(),
LockState: m.stateLock,
StateLockTimeout: m.stateLockTimeout,
}
@ -572,7 +572,7 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
return nil, err
}
env := m.Env()
env := m.Workspace()
// Get the state so we can determine the effect of using this plan
realMgr, err := b.State(env)
@ -967,7 +967,7 @@ func (m *Meta) backend_C_r_s(
return nil, fmt.Errorf(errBackendLocalRead, err)
}
env := m.Env()
env := m.Workspace()
localState, err := localB.State(env)
if err != nil {
@ -1341,14 +1341,18 @@ func (m *Meta) backendInitFromConfig(c *config.Backend) (backend.Backend, error)
// Validate
warns, errs := b.Validate(config)
for _, warning := range warns {
// We just write warnings directly to the UI. This isn't great
// since we're a bit deep here to be pushing stuff out into the
// UI, but sufficient to let us print out deprecation warnings
// and the like.
m.Ui.Warn(warning)
}
if len(errs) > 0 {
return nil, fmt.Errorf(
"Error configuring the backend %q: %s",
c.Type, multierror.Append(nil, errs...))
}
if len(warns) > 0 {
// TODO: warnings are currently ignored
}
// Configure
if err := b.Configure(config); err != nil {

View File

@ -117,7 +117,7 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
migrate, err := m.confirm(&terraform.InputOpts{
Id: "backend-migrate-multistate-to-multistate",
Query: fmt.Sprintf(
"Do you want to migrate all environments to %q?",
"Do you want to migrate all workspaces to %q?",
opts.TwoType),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateMultiToMulti),
@ -162,7 +162,7 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
// Multi-state to single state.
func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
currentEnv := m.Env()
currentEnv := m.Workspace()
migrate := opts.force
if !migrate {
@ -171,8 +171,8 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
migrate, err = m.confirm(&terraform.InputOpts{
Id: "backend-migrate-multistate-to-single",
Query: fmt.Sprintf(
"Destination state %q doesn't support environments (named states).\n"+
"Do you want to copy only your current environment?",
"Destination state %q doesn't support workspaces.\n"+
"Do you want to copy only your current workspace?",
opts.TwoType),
Description: fmt.Sprintf(
strings.TrimSpace(inputBackendMigrateMultiToSingle),
@ -192,7 +192,7 @@ func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
opts.oneEnv = currentEnv
// now switch back to the default env so we can acccess the new backend
m.SetEnv(backend.DefaultStateName)
m.SetWorkspace(backend.DefaultStateName)
return m.backendMigrateState_s_s(opts)
}
@ -458,17 +458,17 @@ above error and try again.
`
const errMigrateMulti = `
Error migrating the environment %q from %q to %q:
Error migrating the workspace %q from %q to %q:
%s
Terraform copies environments in alphabetical order. Any environments
alphabetically earlier than this one have been copied. Any environments
later than this haven't been modified in the destination. No environments
Terraform copies workspaces in alphabetical order. Any workspaces
alphabetically earlier than this one have been copied. Any workspaces
later than this haven't been modified in the destination. No workspaces
in the source state have been modified.
Please resolve the error above and run the initialization command again.
This will attempt to copy (with permission) all environments again.
This will attempt to copy (with permission) all workspaces again.
`
const errBackendStateCopy = `
@ -497,22 +497,22 @@ and "no" to start with the existing state in %[2]q.
`
const inputBackendMigrateMultiToSingle = `
The existing backend %[1]q supports environments and you currently are
using more than one. The target backend %[2]q doesn't support environments.
If you continue, Terraform will offer to copy your current environment
%[3]q to the default environment in the target. Your existing environments
in the source backend won't be modified. If you want to switch environments,
The existing backend %[1]q supports workspaces and you currently are
using more than one. The target backend %[2]q doesn't support workspaces.
If you continue, Terraform will offer to copy your current workspace
%[3]q to the default workspace in the target. Your existing workspaces
in the source backend won't be modified. If you want to switch workspaces,
back them up, or cancel altogether, answer "no" and Terraform will abort.
`
const inputBackendMigrateMultiToMulti = `
Both the existing backend %[1]q and the target backend %[2]q support
environments. When migrating between backends, Terraform will copy all
environments (with the same names). THIS WILL OVERWRITE any conflicting
workspaces. When migrating between backends, Terraform will copy all
workspaces (with the same names). THIS WILL OVERWRITE any conflicting
states in the destination.
Terraform initialization doesn't currently migrate only select environments.
If you want to migrate a select number of environments, you must manually
Terraform initialization doesn't currently migrate only select workspaces.
If you want to migrate a select number of workspaces, you must manually
pull and push those states.
If you answer "yes", Terraform will migrate all states. If you answer

View File

@ -1249,14 +1249,14 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
t.Fatal("file should not exist")
}
// Verify existing environments exist
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
// Verify we are now in the default env, or we may not be able to access the new backend
if env := m.Env(); env != backend.DefaultStateName {
if env := m.Workspace(); env != backend.DefaultStateName {
t.Fatal("using non-default env with single-env backend")
}
}
@ -1285,7 +1285,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
m := testMetaBackend(t, nil)
// Change env
if err := m.SetEnv("env2"); err != nil {
if err := m.SetWorkspace("env2"); err != nil {
t.Fatalf("bad: %s", err)
}
@ -1321,8 +1321,8 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T)
t.Fatal("file should not exist")
}
// Verify existing environments exist
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
@ -1406,15 +1406,15 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
}
{
// Verify existing environments exist
envPath := filepath.Join(backendlocal.DefaultEnvDir, "env2", backendlocal.DefaultStateFilename)
// Verify existing workspaces exist
envPath := filepath.Join(backendlocal.DefaultWorkspaceDir, "env2", backendlocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")
}
}
{
// Verify new environments exist
// Verify new workspaces exist
envPath := filepath.Join("envdir-new", "env2", backendlocal.DefaultStateFilename)
if _, err := os.Stat(envPath); err != nil {
t.Fatal("env should exist")

View File

@ -282,27 +282,27 @@ func TestMeta_Env(t *testing.T) {
m := new(Meta)
env := m.Env()
env := m.Workspace()
if env != backend.DefaultStateName {
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
}
testEnv := "test_env"
if err := m.SetEnv(testEnv); err != nil {
if err := m.SetWorkspace(testEnv); err != nil {
t.Fatal("error setting env:", err)
}
env = m.Env()
env = m.Workspace()
if env != testEnv {
t.Fatalf("expected env %q, got env %q", testEnv, env)
}
if err := m.SetEnv(backend.DefaultStateName); err != nil {
if err := m.SetWorkspace(backend.DefaultStateName); err != nil {
t.Fatal("error setting env:", err)
}
env = m.Env()
env = m.Workspace()
if env != backend.DefaultStateName {
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
}

View File

@ -50,7 +50,7 @@ func (c *OutputCommand) Run(args []string) int {
return 1
}
env := c.Env()
env := c.Workspace()
// Get the state
stateStore, err := b.State(env)

View File

@ -61,7 +61,7 @@ func (c *ProvidersCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
state, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -51,8 +51,8 @@ func (c *PushCommand) Run(args []string) int {
// This is a map of variables specifically from the CLI that we want to overwrite.
// We need this because there is a chance that the user is trying to modify
// a variable we don't see in our context, but which exists in this atlas
// environment.
// a variable we don't see in our context, but which exists in this Terraform
// Enterprise workspace.
cliVars := make(map[string]string)
for k, v := range c.variables {
if _, ok := overwriteMap[k]; ok {

View File

@ -74,7 +74,7 @@ func (c *ShowCommand) Run(args []string) int {
return 1
}
env := c.Env()
env := c.Workspace()
// Get the state
stateStore, err := b.State(env)

View File

@ -32,7 +32,7 @@ func (c *StateListCommand) Run(args []string) int {
return 1
}
env := c.Env()
env := c.Workspace()
// Get the state
state, err := b.State(env)
if err != nil {

View File

@ -24,7 +24,7 @@ func (c *StateMeta) State(m *Meta) (state.State, error) {
return nil, err
}
env := m.Env()
env := m.Workspace()
// Get the state
s, err := b.State(env)
if err != nil {

View File

@ -32,7 +32,7 @@ func (c *StatePullCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
state, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -67,7 +67,7 @@ func (c *StatePushCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
state, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))

View File

@ -34,7 +34,7 @@ func (c *StateShowCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
state, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -69,7 +69,7 @@ func (c *TaintCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
st, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -58,7 +58,7 @@ func (c *UnlockCommand) Run(args []string) int {
return 1
}
env := c.Env()
env := c.Workspace()
st, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -57,7 +57,7 @@ func (c *UntaintCommand) Run(args []string) int {
}
// Get the state
env := c.Env()
env := c.Workspace()
st, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -0,0 +1,144 @@
package command
import (
"net/url"
"strings"
"github.com/mitchellh/cli"
)
// WorkspaceCommand is a Command Implementation that manipulates workspaces,
// which allow multiple distinct states and variables from a single config.
type WorkspaceCommand struct {
Meta
LegacyName bool
}
func (c *WorkspaceCommand) Run(args []string) int {
args = c.Meta.process(args, true)
envCommandShowWarning(c.Ui, c.LegacyName)
cmdFlags := c.Meta.flagSet("workspace")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
c.Ui.Output(c.Help())
return 0
}
func (c *WorkspaceCommand) Help() string {
helpText := `
Usage: terraform workspace
Create, change and delete Terraform workspaces.
Subcommands:
list List workspaces.
select Select a workspace.
new Create a new workspace.
delete Delete an existing workspace.
`
return strings.TrimSpace(helpText)
}
func (c *WorkspaceCommand) Synopsis() string {
return "Workspace management"
}
// validWorkspaceName returns true is this name is valid to use as a workspace name.
// Since most named states are accessed via a filesystem path or URL, check if
// escaping the name would be required.
func validWorkspaceName(name string) bool {
return name == url.PathEscape(name)
}
func envCommandShowWarning(ui cli.Ui, show bool) {
if !show {
return
}
ui.Warn(`Warning: the "terraform env" family of commands is deprecated.
"Workspace" is now the preferred term for what earlier Terraform versions
called "environment", to reduce ambiguity caused by the latter term colliding
with other concepts.
The "terraform workspace" commands should be used instead. "terraform env"
will be removed in a future Terraform version.
`)
}
const (
envNotSupported = `Backend does not support multiple workspaces`
envExists = `Workspace %q already exists`
envDoesNotExist = `
Workspace %q doesn't exist.
You can create this workspace with the "new" subcommand.`
envChanged = `[reset][green]Switched to workspace %q.`
envCreated = `
[reset][green][bold]Created and switched to workspace %q![reset][green]
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
`
envDeleted = `[reset][green]Deleted workspace %q!`
envNotEmpty = `
Workspace %[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 workspace
anyway and risk dangling resources, use the '-force' flag.
`
envWarnNotEmpty = `[reset][yellow]WARNING: %q was non-empty.
The resources managed by the deleted workspace may still exist,
but are no longer manageable by Terraform since the state has
been deleted.
`
envDelCurrent = `
Workspace %[1]q is your active workspace.
You cannot delete the currently active workspace. Please switch
to another workspace and try again.
`
envInvalidName = `
The workspace name %q is not allowed. The name must contain only URL safe
characters, and no path separators.
`
envIsOverriddenNote = `
The active workspace is being overridden using the TF_WORKSPACE environment
variable.
`
envIsOverriddenSelectError = `
The selected workspace is currently overridden using the TF_WORKSPACE
environment variable.
To select a new workspace, either update this environment variable or unset
it and then run this command again.
`
envIsOverriddenNewError = `
The workspace is currently overridden using the TF_WORKSPACE environment
variable. You cannot create a new workspace when using this setting.
To create a new workspace, either unset this environment variable or update it
to match the workspace name you are trying to create, and then run this command
again.
`
)

View File

@ -14,18 +14,18 @@ import (
"github.com/mitchellh/cli"
)
func TestEnv_createAndChange(t *testing.T) {
func TestWorkspace_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)()
newCmd := &EnvNewCommand{}
newCmd := &WorkspaceNewCommand{}
current := newCmd.Env()
current := newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current env should be 'default'")
t.Fatal("current workspace should be 'default'")
}
args := []string{"test"}
@ -35,12 +35,12 @@ func TestEnv_createAndChange(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
current = newCmd.Env()
current = newCmd.Workspace()
if current != "test" {
t.Fatalf("current env should be 'test', got %q", current)
t.Fatalf("current workspace should be 'test', got %q", current)
}
selCmd := &EnvSelectCommand{}
selCmd := &WorkspaceSelectCommand{}
args = []string{backend.DefaultStateName}
ui = new(cli.MockUi)
selCmd.Meta = Meta{Ui: ui}
@ -48,16 +48,16 @@ func TestEnv_createAndChange(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
current = newCmd.Env()
current = newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current env should be 'default'")
t.Fatal("current workspace should be 'default'")
}
}
// Create some environments and test the list output.
// Create some workspaces and test the list output.
// This also ensures we switch to the correct env after each call
func TestEnv_createAndList(t *testing.T) {
func TestWorkspace_createAndList(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
os.MkdirAll(td, 0755)
@ -74,11 +74,11 @@ func TestEnv_createAndList(t *testing.T) {
t.Fatal(err)
}
newCmd := &EnvNewCommand{}
newCmd := &WorkspaceNewCommand{}
envs := []string{"test_a", "test_b", "test_c"}
// create multiple envs
// create multiple workspaces
for _, env := range envs {
ui := new(cli.MockUi)
newCmd.Meta = Meta{Ui: ui}
@ -87,7 +87,7 @@ func TestEnv_createAndList(t *testing.T) {
}
}
listCmd := &EnvListCommand{}
listCmd := &WorkspaceListCommand{}
ui := new(cli.MockUi)
listCmd.Meta = Meta{Ui: ui}
@ -104,18 +104,18 @@ func TestEnv_createAndList(t *testing.T) {
}
// Don't allow names that aren't URL safe
func TestEnv_createInvalid(t *testing.T) {
func TestWorkspace_createInvalid(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)()
newCmd := &EnvNewCommand{}
newCmd := &WorkspaceNewCommand{}
envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"}
// create multiple envs
// create multiple workspaces
for _, env := range envs {
ui := new(cli.MockUi)
newCmd.Meta = Meta{Ui: ui}
@ -124,8 +124,8 @@ func TestEnv_createInvalid(t *testing.T) {
}
}
// list envs to make sure none were created
listCmd := &EnvListCommand{}
// list workspaces to make sure none were created
listCmd := &WorkspaceListCommand{}
ui := new(cli.MockUi)
listCmd.Meta = Meta{Ui: ui}
@ -141,7 +141,7 @@ func TestEnv_createInvalid(t *testing.T) {
}
}
func TestEnv_createWithState(t *testing.T) {
func TestWorkspace_createWithState(t *testing.T) {
td := tempDir(t)
os.MkdirAll(td, 0755)
defer os.RemoveAll(td)
@ -171,14 +171,14 @@ func TestEnv_createWithState(t *testing.T) {
args := []string{"-state", "test.tfstate", "test"}
ui := new(cli.MockUi)
newCmd := &EnvNewCommand{
newCmd := &WorkspaceNewCommand{
Meta: Meta{Ui: ui},
}
if code := newCmd.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
}
newPath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
envState := state.LocalState{Path: newPath}
err = envState.RefreshState()
if err != nil {
@ -191,43 +191,43 @@ func TestEnv_createWithState(t *testing.T) {
}
}
func TestEnv_delete(t *testing.T) {
func TestWorkspace_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 {
// create the workspace directories
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
t.Fatal(err)
}
// create the environment file
// create the workspace 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 {
if err := ioutil.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
delCmd := &EnvDeleteCommand{
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui},
}
current := delCmd.Env()
current := delCmd.Workspace()
if current != "test" {
t.Fatal("wrong env:", current)
t.Fatal("wrong workspace:", current)
}
// we can't delete out current environment
// we can't delete our current workspace
args := []string{"test"}
if code := delCmd.Run(args); code == 0 {
t.Fatal("expected error deleting current env")
t.Fatal("expected error deleting current workspace")
}
// change back to default
if err := delCmd.SetEnv(backend.DefaultStateName); err != nil {
if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil {
t.Fatal(err)
}
@ -235,22 +235,22 @@ func TestEnv_delete(t *testing.T) {
ui = new(cli.MockUi)
delCmd.Meta.Ui = ui
if code := delCmd.Run(args); code != 0 {
t.Fatalf("error deleting env: %s", ui.ErrorWriter)
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
}
current = delCmd.Env()
current = delCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatalf("wrong env: %q", current)
t.Fatalf("wrong workspace: %q", current)
}
}
func TestEnv_deleteWithState(t *testing.T) {
func TestWorkspace_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 {
// create the workspace directories
if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil {
t.Fatal(err)
}
@ -271,14 +271,14 @@ func TestEnv_deleteWithState(t *testing.T) {
},
}
envStatePath := filepath.Join(local.DefaultEnvDir, "test", DefaultStateFilename)
envStatePath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename)
err := (&state.LocalState{Path: envStatePath}).WriteState(originalState)
if err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
delCmd := &EnvDeleteCommand{
delCmd := &WorkspaceDeleteCommand{
Meta: Meta{Ui: ui},
}
args := []string{"test"}
@ -294,7 +294,7 @@ func TestEnv_deleteWithState(t *testing.T) {
t.Fatalf("failure: %s", ui.ErrorWriter)
}
if _, err := os.Stat(filepath.Join(local.DefaultEnvDir, "test")); !os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) {
t.Fatal("env 'test' still exists!")
}
}

View File

@ -10,16 +10,19 @@ import (
"github.com/mitchellh/cli"
)
type EnvDeleteCommand struct {
type WorkspaceDeleteCommand struct {
Meta
LegacyName bool
}
func (c *EnvDeleteCommand) Run(args []string) int {
func (c *WorkspaceDeleteCommand) Run(args []string) int {
args = c.Meta.process(args, true)
envCommandShowWarning(c.Ui, c.LegacyName)
force := false
cmdFlags := c.Meta.flagSet("env")
cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty environment")
cmdFlags := c.Meta.flagSet("workspace")
cmdFlags.BoolVar(&force, "force", false, "force removal of a non-empty workspace")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -32,7 +35,7 @@ func (c *EnvDeleteCommand) Run(args []string) int {
delEnv := args[0]
if !validEnvName(delEnv) {
if !validWorkspaceName(delEnv) {
c.Ui.Error(fmt.Sprintf(envInvalidName, delEnv))
return 1
}
@ -78,7 +81,7 @@ func (c *EnvDeleteCommand) Run(args []string) int {
return 1
}
if delEnv == c.Env() {
if delEnv == c.Workspace() {
c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envDelCurrent), delEnv))
return 1
}
@ -108,7 +111,7 @@ func (c *EnvDeleteCommand) Run(args []string) int {
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "env delete"
lockInfo.Operation = "workspace delete"
lockID, err := clistate.Lock(lockCtx, sMgr, lockInfo, c.Ui, c.Colorize())
if err != nil {
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))
@ -139,20 +142,20 @@ func (c *EnvDeleteCommand) Run(args []string) int {
return 0
}
func (c *EnvDeleteCommand) Help() string {
func (c *WorkspaceDeleteCommand) Help() string {
helpText := `
Usage: terraform env delete [OPTIONS] NAME [DIR]
Usage: terraform workspace delete [OPTIONS] NAME [DIR]
Delete a Terraform environment
Delete a Terraform workspace
Options:
-force remove a non-empty environment.
-force remove a non-empty workspace.
`
return strings.TrimSpace(helpText)
}
func (c *EnvDeleteCommand) Synopsis() string {
return "Delete an environment"
func (c *WorkspaceDeleteCommand) Synopsis() string {
return "Delete a workspace"
}

View File

@ -6,14 +6,17 @@ import (
"strings"
)
type EnvListCommand struct {
type WorkspaceListCommand struct {
Meta
LegacyName bool
}
func (c *EnvListCommand) Run(args []string) int {
func (c *WorkspaceListCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("env list")
envCommandShowWarning(c.Ui, c.LegacyName)
cmdFlags := c.Meta.flagSet("workspace list")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -48,7 +51,7 @@ func (c *EnvListCommand) Run(args []string) int {
return 1
}
env := c.Env()
env, isOverridden := c.WorkspaceOverridden()
var out bytes.Buffer
for _, s := range states {
@ -61,18 +64,23 @@ func (c *EnvListCommand) Run(args []string) int {
}
c.Ui.Output(out.String())
if isOverridden {
c.Ui.Output(envIsOverriddenNote)
}
return 0
}
func (c *EnvListCommand) Help() string {
func (c *WorkspaceListCommand) Help() string {
helpText := `
Usage: terraform env list [DIR]
Usage: terraform workspace list [DIR]
List Terraform environments.
List Terraform workspaces.
`
return strings.TrimSpace(helpText)
}
func (c *EnvListCommand) Synopsis() string {
return "List Environments"
func (c *WorkspaceListCommand) Synopsis() string {
return "List Workspaces"
}

View File

@ -12,16 +12,19 @@ import (
"github.com/mitchellh/cli"
)
type EnvNewCommand struct {
type WorkspaceNewCommand struct {
Meta
LegacyName bool
}
func (c *EnvNewCommand) Run(args []string) int {
func (c *WorkspaceNewCommand) Run(args []string) int {
args = c.Meta.process(args, true)
envCommandShowWarning(c.Ui, c.LegacyName)
statePath := ""
cmdFlags := c.Meta.flagSet("env new")
cmdFlags := c.Meta.flagSet("workspace new")
cmdFlags.StringVar(&statePath, "state", "", "terraform state file")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
@ -35,11 +38,18 @@ func (c *EnvNewCommand) Run(args []string) int {
newEnv := args[0]
if !validEnvName(newEnv) {
if !validWorkspaceName(newEnv) {
c.Ui.Error(fmt.Sprintf(envInvalidName, newEnv))
return 1
}
// You can't ask to create a workspace when you're overriding the
// workspace name to be something different.
if current, isOverridden := c.WorkspaceOverridden(); current != newEnv && isOverridden {
c.Ui.Error(envIsOverriddenNewError)
return 1
}
configPath, err := ModulePath(args[1:])
if err != nil {
c.Ui.Error(err.Error())
@ -75,9 +85,9 @@ func (c *EnvNewCommand) Run(args []string) int {
return 1
}
// now save the current env locally
if err := c.SetEnv(newEnv); err != nil {
c.Ui.Error(fmt.Sprintf("error saving new environment name: %s", err))
// now set the current workspace locally
if err := c.SetWorkspace(newEnv); err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting new workspace: %s", err))
return 1
}
@ -102,7 +112,7 @@ func (c *EnvNewCommand) Run(args []string) int {
// Lock the state if we can
lockInfo := state.NewLockInfo()
lockInfo.Operation = "env new"
lockInfo.Operation = "workspace new"
lockID, err := clistate.Lock(lockCtx, sMgr, lockInfo, c.Ui, c.Colorize())
if err != nil {
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))
@ -139,20 +149,20 @@ func (c *EnvNewCommand) Run(args []string) int {
return 0
}
func (c *EnvNewCommand) Help() string {
func (c *WorkspaceNewCommand) Help() string {
helpText := `
Usage: terraform env new [OPTIONS] NAME [DIR]
Usage: terraform workspace new [OPTIONS] NAME [DIR]
Create a new Terraform environment.
Create a new Terraform workspace.
Options:
-state=path Copy an existing state file into the new environment.
-state=path Copy an existing state file into the new workspace.
`
return strings.TrimSpace(helpText)
}
func (c *EnvNewCommand) Synopsis() string {
return "Create a new environment"
func (c *WorkspaceNewCommand) Synopsis() string {
return "Create a new workspace"
}

View File

@ -7,14 +7,17 @@ import (
"github.com/mitchellh/cli"
)
type EnvSelectCommand struct {
type WorkspaceSelectCommand struct {
Meta
LegacyName bool
}
func (c *EnvSelectCommand) Run(args []string) int {
func (c *WorkspaceSelectCommand) Run(args []string) int {
args = c.Meta.process(args, true)
cmdFlags := c.Meta.flagSet("env select")
envCommandShowWarning(c.Ui, c.LegacyName)
cmdFlags := c.Meta.flagSet("workspace select")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -34,6 +37,11 @@ func (c *EnvSelectCommand) Run(args []string) int {
conf, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
}
current, isOverridden := c.WorkspaceOverridden()
if isOverridden {
c.Ui.Error(envIsOverriddenSelectError)
return 1
}
@ -48,7 +56,7 @@ func (c *EnvSelectCommand) Run(args []string) int {
}
name := args[0]
if !validEnvName(name) {
if !validWorkspaceName(name) {
c.Ui.Error(fmt.Sprintf(envInvalidName, name))
return 1
}
@ -59,8 +67,8 @@ func (c *EnvSelectCommand) Run(args []string) int {
return 1
}
if name == c.Env() {
// already using this env
if name == current {
// already using this workspace
return 0
}
@ -77,7 +85,7 @@ func (c *EnvSelectCommand) Run(args []string) int {
return 1
}
err = c.SetEnv(name)
err = c.SetWorkspace(name)
if err != nil {
c.Ui.Error(err.Error())
return 1
@ -92,15 +100,15 @@ func (c *EnvSelectCommand) Run(args []string) int {
return 0
}
func (c *EnvSelectCommand) Help() string {
func (c *WorkspaceSelectCommand) Help() string {
helpText := `
Usage: terraform env select NAME [DIR]
Usage: terraform workspace select NAME [DIR]
Change Terraform environment.
Select a different Terraform workspace.
`
return strings.TrimSpace(helpText)
}
func (c *EnvSelectCommand) Synopsis() string {
return "Change environments"
func (c *WorkspaceSelectCommand) Synopsis() string {
return "Select a workspace"
}

View File

@ -72,32 +72,37 @@ func init() {
},
"env": func() (cli.Command, error) {
return &command.EnvCommand{
Meta: meta,
return &command.WorkspaceCommand{
Meta: meta,
LegacyName: true,
}, nil
},
"env list": func() (cli.Command, error) {
return &command.EnvListCommand{
Meta: meta,
return &command.WorkspaceListCommand{
Meta: meta,
LegacyName: true,
}, nil
},
"env select": func() (cli.Command, error) {
return &command.EnvSelectCommand{
Meta: meta,
return &command.WorkspaceSelectCommand{
Meta: meta,
LegacyName: true,
}, nil
},
"env new": func() (cli.Command, error) {
return &command.EnvNewCommand{
Meta: meta,
return &command.WorkspaceNewCommand{
Meta: meta,
LegacyName: true,
}, nil
},
"env delete": func() (cli.Command, error) {
return &command.EnvDeleteCommand{
Meta: meta,
return &command.WorkspaceDeleteCommand{
Meta: meta,
LegacyName: true,
}, nil
},
@ -201,6 +206,36 @@ func init() {
}, nil
},
"workspace": func() (cli.Command, error) {
return &command.WorkspaceCommand{
Meta: meta,
}, nil
},
"workspace list": func() (cli.Command, error) {
return &command.WorkspaceListCommand{
Meta: meta,
}, nil
},
"workspace select": func() (cli.Command, error) {
return &command.WorkspaceSelectCommand{
Meta: meta,
}, nil
},
"workspace new": func() (cli.Command, error) {
return &command.WorkspaceNewCommand{
Meta: meta,
}, nil
},
"workspace delete": func() (cli.Command, error) {
return &command.WorkspaceDeleteCommand{
Meta: meta,
}, nil
},
//-----------------------------------------------------------
// Plumbing
//-----------------------------------------------------------

View File

@ -317,9 +317,13 @@ func (i *Interpolater) valueTerraformVar(
n string,
v *config.TerraformVariable,
result map[string]ast.Variable) error {
if v.Field != "env" {
// "env" is supported for backward compatibility, but it's deprecated and
// so we won't advertise it as being allowed in the error message. It will
// be removed in a future version of Terraform.
if v.Field != "workspace" && v.Field != "env" {
return fmt.Errorf(
"%s: only supported key for 'terraform.X' interpolations is 'env'", n)
"%s: only supported key for 'terraform.X' interpolations is 'workspace'", n)
}
if i.Meta == nil {

View File

@ -0,0 +1,13 @@
---
layout: "docs"
page_title: "Command: env"
sidebar_current: "docs-commands-env"
description: |-
The terraform env command is a deprecated, legacy form of "terraform workspace".
---
# Command: env
The `terraform env` command is deprecated.
[The `terraform workspace` command](/docs/commands/workspace/)
should be used instead.

View File

@ -1,39 +0,0 @@
---
layout: "commands-env"
page_title: "Command: env delete"
sidebar_current: "docs-env-sub-delete"
description: |-
The terraform env delete command is used to create a delete state environment.
---
# Command: env delete
The `terraform env delete` command is used to delete an existing environment.
## Usage
Usage: `terraform env delete [NAME]`
This command will delete the specified environment.
To delete an environment, it must already exist, it must be empty, and
it must not be your current environment. If the environment
is not empty, Terraform will not allow you to delete it without the
`-force` flag.
If you delete a non-empty state (via force), then resources may become
"dangling". These are resources that Terraform no longer manages since
a state doesn't point to them, but still physically exist. This is sometimes
preferred: you want Terraform to stop managing resources. Most of the time,
however, this is not intended so Terraform protects you from doing this.
The command-line flags are all optional. The list of available flags are:
* `-force` - Delete the state even if non-empty. Defaults to false.
## Example
```
$ terraform env delete example
Deleted environment "example"!
```

View File

@ -1,21 +0,0 @@
---
layout: "commands-env"
page_title: "Command: env"
sidebar_current: "docs-env-index"
description: |-
The `terraform env` command is used to manage state environments.
---
# Env Command
The `terraform env` command is used to manage
[state environments](/docs/state/environments.html).
This command is a nested subcommand, meaning that it has further subcommands.
These subcommands are listed to the left.
## Usage
Usage: `terraform env <subcommand> [options] [args]`
Please click a subcommand to the left for more information.

View File

@ -1,27 +0,0 @@
---
layout: "commands-env"
page_title: "Command: env list"
sidebar_current: "docs-env-sub-list"
description: |-
The terraform env list command is used to list all created state environments.
---
# Command: env list
The `terraform env list` command is used to list all created state environments.
## Usage
Usage: `terraform env list`
The command will list all created environments. The current environment
will have an asterisk (`*`) next to it.
## Example
```
$ terraform env list
default
* development
mitchellh-test
```

View File

@ -1,50 +0,0 @@
---
layout: "commands-env"
page_title: "Command: env new"
sidebar_current: "docs-env-sub-new"
description: |-
The terraform env new command is used to create a new state environment.
---
# Command: env new
The `terraform env new` command is used to create a new state
environment.
## Usage
Usage: `terraform env new [NAME]`
This command will create a new environment with the given name. This
environment must not already exist.
If the `-state` flag is given, the state specified by the given path
will be copied to initialize the state for this new environment.
The command-line flags are all optional. The list of available flags are:
* `-state=path` - Path to a state file to initialize the state of this environment.
## Example: Create
```
$ terraform env new example
Created and switched to environment "example"!
You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```
## Example: Create from State
To create a new environment from a pre-existing state path:
```
$ terraform env new -state=old.terraform.tfstate example
Created and switched to environment "example"!
You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```

View File

@ -1,31 +0,0 @@
---
layout: "commands-env"
page_title: "Command: env select"
sidebar_current: "docs-env-sub-select"
description: |-
The terraform env select command is used to select state environments.
---
# Command: env select
The `terraform env select` command is used to select to a different
environment that is already created.
## Usage
Usage: `terraform env select [NAME]`
This command will select to another environment. The environment must
already be created.
## Example
```
$ terraform env list
default
* development
mitchellh-test
$ terraform env select default
Switched to environment "default"!
```

View File

@ -33,7 +33,6 @@ Common commands:
apply Builds or changes infrastructure
console Interactive console for Terraform interpolations
destroy Destroy Terraform-managed infrastructure
env Environment management
fmt Rewrites config files to canonical format
get Download and install modules for the configuration
graph Create a visual graph of Terraform resources
@ -49,6 +48,7 @@ Common commands:
untaint Manually unmark a resource as tainted
validate Validates the Terraform files
version Prints the Terraform version
workspace Workspace management
All other commands:
debug Debug output management (experimental)

View File

@ -0,0 +1,39 @@
---
layout: "commands-workspace"
page_title: "Command: workspace delete"
sidebar_current: "docs-workspace-sub-delete"
description: |-
The terraform workspace delete command is used to delete a workspace.
---
# Command: workspace delete
The `terraform workspace delete` command is used to delete an existing workspace.
## Usage
Usage: `terraform workspace delete [NAME]`
This command will delete the specified workspace.
To delete an workspace, it must already exist, it must have an empty state,
and it must not be your current workspace. If the workspace state is not empty,
Terraform will not allow you to delete it unless the `-force` flag is specified.
If you delete a workspace with a non-empty state (via `-force`), then resources
may become "dangling". These are resources that physically exist but that
Terraform can no longer manage. This is sometimes preferred: you want
Terraform to stop managing resources so they can be managed some other way.
Most of the time, however, this is not intended and so Terraform protects you
from getting into this situation.
The command-line flags are all optional. The only supported flag is:
* `-force` - Delete the workspace even if its state is not empty. Defaults to false.
## Example
```
$ terraform workspace delete example
Deleted workspace "example".
```

View File

@ -0,0 +1,21 @@
---
layout: "commands-workspace"
page_title: "Command: workspace"
sidebar_current: "docs-workspace-index"
description: |-
The terraform workspace command is used to manage workspaces.
---
# Command: workspace
The `terraform workspace` command is used to manage
[workspaces](/docs/state/workspaces.html).
This command is a container for further subcommands. These subcommands are
listed in the navigation bar.
## Usage
Usage: `terraform workspace <subcommand> [options] [args]`
Please choose a subcommand from the navigation for more information.

View File

@ -0,0 +1,27 @@
---
layout: "commands-workspace"
page_title: "Command: workspace list"
sidebar_current: "docs-workspace-sub-list"
description: |-
The terraform workspace list command is used to list all existing workspaces.
---
# Command: workspace list
The `terraform workspace list` command is used to list all existing workspaces.
## Usage
Usage: `terraform workspace list`
The command will list all existing workspaces. The current workspace is
indicated using an asterisk (`*`) marker.
## Example
```
$ terraform workspace list
default
* development
jsmith-test
```

View File

@ -0,0 +1,49 @@
---
layout: "commands-workspace"
page_title: "Command: workspace new"
sidebar_current: "docs-workspace-sub-new"
description: |-
The terraform workspace new command is used to create a new workspace.
---
# Command: workspace new
The `terraform workspace new` command is used to create a new workspace.
## Usage
Usage: `terraform workspace new [NAME]`
This command will create a new workspace with the given name. A workspace with
this name must not already exist.
If the `-state` flag is given, the state specified by the given path
will be copied to initialize the state for this new workspace.
The command-line flags are all optional. The only supported flag is:
* `-state=path` - Path to a state file to initialize the state of this environment.
## Example: Create
```
$ terraform workspace new example
Created and switched to workspace "example"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```
## Example: Create from State
To create a new workspace from a pre-existing local state file:
```
$ terraform workspace new -state=old.terraform.tfstate example
Created and switched to workspace "example".
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```

View File

@ -0,0 +1,31 @@
---
layout: "commands-workspace"
page_title: "Command: workspace select"
sidebar_current: "docs-workspace-sub-select"
description: |-
The terraform workspace select command is used to choose a workspace.
---
# Command: env select
The `terraform workspace select` command is used to choose a different
workspace to use for further operations.
## Usage
Usage: `terraform workspace select [NAME]`
This command will select another workspace. The named workspace must already
exist.
## Example
```
$ terraform workspace list
default
* development
jsmith-test
$ terraform workspace select default
Switched to workspace "default".
```

View File

@ -31,9 +31,9 @@ resource "aws_instance" "foo" {
The following arguments are supported:
* `backend` - (Required) The remote backend to use.
* `environment` - (Optional) The Terraform environment to use.
* `config` - (Optional) The configuration of the remote backend.
* Remote state config docs can be found [here](/docs/backends/types/terraform-enterprise.html)
* `workspace` - (Optional) The Terraform workspace whose state will be requested. Defaults to "default".
* `config` - (Optional) The configuration of the remote backend. For more information,
see [the Backend Types documentation](/docs/backends/types/).
## Attributes Reference

View File

@ -3,137 +3,18 @@ layout: "docs"
page_title: "State: Environments"
sidebar_current: "docs-state-env"
description: |-
Terraform stores state which caches the known state of the world the last time Terraform ran.
Legacy terminology for "Workspaces".
---
# State Environments
An environment is a state namespace, allowing a single folder of Terraform
configurations to manage multiple distinct infrastructure resources.
The term _state environment_, or just _environment_, was used within the
Terraform 0.9 releases to refer to the idea of having multiple distinct,
named states associated with a single configuration directory.
Terraform state determines what resources it manages based on what
exists in the state. This is how `terraform plan` determines what isn't
created, what needs to be updated, etc. The full details of state can be
found on the [purpose page](/docs/state/purpose.html).
After this concept was implemented, we recieved feedback that this terminology
caused confusion due to other uses of the word "environment", both within
Terraform itself and within organizations using Terraform.
Environments are a way to create multiple states that contain
their own data so a single set of Terraform configurations can manage
multiple distinct sets of resources.
Environments are currently supported by the following backends:
* [Consul](/docs/backends/types/consul.html)
* [S3](/docs/backends/types/s3.html)
## Using Environments
Terraform starts with a single environment named "default". This
environment is special both because it is the default and also because
it cannot ever be deleted. If you've never explicitly used environments, then
you've only ever worked on the "default" environment.
Environments are managed with the `terraform env` set of commands. To
create a new environment and switch to it, you can use `terraform env new`,
to switch environments you can use `terraform env select`, etc.
For example, creating an environment:
```text
$ terraform env new bar
Created and switched to environment "bar"!
You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```
As the command says, if you run `terraform plan`, Terraform will not see
any existing resources that existed on the default (or any other) environment.
**These resources still physically exist,** but are managed by another
Terraform environment.
## Current Environment Interpolation
Within your Terraform configuration, you may reference the current environment
using the `${terraform.env}` interpolation variable. This can be used anywhere
interpolations are allowed.
Referencing the current environment is useful for changing behavior based
on the environment. For example, for non-default environments, it may be useful
to spin up smaller cluster sizes. You can do this:
```hcl
resource "aws_instance" "example" {
count = "${terraform.env == "default" ? 5 : 1}"
# ... other fields
}
```
Another popular use case is using the environment as part of naming or
tagging behavior:
```hcl
resource "aws_instance" "example" {
tags { Name = "web - ${terraform.env}" }
# ... other fields
}
```
## Best Practices
An environment can be used to manage the difference between development,
staging, and production, but it **should not** be treated as the only isolation
mechanism. As Terraform configurations get larger, it's much more
manageable and safer to split one large configuration into many
smaller ones linked together with `terraform_remote_state` data sources. This
allows teams to delegate ownership and reduce the blast radius of changes.
For **each smaller configuration**, you can use environments to model the
differences between development, staging, and production. However, if you have
one large Terraform configuration, it is riskier and not recommended to use
environments to model those differences.
The [terraform_remote_state](/docs/providers/terraform/d/remote_state.html)
resource accepts an `environment` name to target. Therefore, you can link
together multiple independently managed Terraform configurations with the same
environment easily. But, they can also have different environments.
While environments are available to all,
[Terraform Enterprise](https://www.hashicorp.com/products/terraform/)
provides an interface and API for managing sets of configurations linked
with `terraform_remote_state` and viewing them all as a single environment.
Environments alone are useful for isolating a set of resources to test
changes during development. For example, it is common to associate a
branch in a VCS with an environment so new features can be developed
without affecting the default environment.
Future Terraform versions and environment enhancements will enable
Terraform to track VCS branches with an environment to help verify only certain
branches can make changes to a Terraform environment.
## Environments Internals
Environments are technically equivalent to renaming your state file. They
aren't any more complex than that. Terraform wraps this simple notion with
a set of protections and support for remote state.
For local state, Terraform stores the state environments in a folder
`terraform.tfstate.d`. This folder should be committed to version control
(just like local-only `terraform.tfstate`).
For [remote state](/docs/state/remote.html), the environments are stored
directly in the configured [backend](/docs/backends). For example, if you
use [Consul](/docs/backends/types/consul.html), the environments are stored
by suffixing the state path with the environment name. To ensure that
environment names are stored correctly and safely in all backends, the name
must be valid to use in a URL path segment without escaping.
The important thing about environment internals is that environments are
meant to be a shared resource. They aren't a private, local-only notion
(unless you're using purely local state and not committing it).
The "current environment" name is stored only locally in the ignored
`.terraform` directory. This allows multiple team members to work on
different environments concurrently.
As of 0.10, the preferred term is "workspace". For more information on
workspaces, see [the main Workspaces page](/docs/state/workspaces.html).

View File

@ -0,0 +1,145 @@
---
layout: "docs"
page_title: "State: Workspaces"
sidebar_current: "docs-state-workspaces"
description: |-
Workspaces allow the use of multiple states with a single configuration directory.
---
# Workspaces
A _workspace_ is a named container for Terraform state. With multiple
workspaces, a single directory of Terraform configuration can be used to
manage multiple distinct sets of infrastructure resources.
Terraform state determines what resources it manages based on what
exists in the state. This is how `terraform plan` determines what isn't
created, what needs to be updated, etc. The full details of state can be
found on [the _purpose_ page](/docs/state/purpose.html).
Multiple workspaces are currently supported by the following backends:
* [Consul](/docs/backends/types/consul.html)
* [S3](/docs/backends/types/s3.html)
In the 0.9 line of Terraform releases, this concept was known as "environment".
It was renamed in 0.10 based on feedback about confusion caused by the
overloading of the word "environment" both within Terraform itself and within
organizations that use Terraform.
## Using Workspaces
Terraform starts with a single workspace named "default". This
workspace is special both because it is the default and also because
it cannot ever be deleted. If you've never explicitly used workspaces, then
you've only ever worked on the "default" workspace.
Workspaces are managed with the `terraform workspace` set of commands. To
create a new workspace and switch to it, you can use `terraform workspace new`;
to switch environments you can use `terraform workspace select`; etc.
For example, creating a new workspace:
```text
$ terraform workspace new bar
Created and switched to workspace "bar"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
```
As the command says, if you run `terraform plan`, Terraform will not see
any existing resources that existed on the default (or any other) workspace.
**These resources still physically exist,** but are managed in another
Terraform workspace.
## Current Workspace Interpolation
Within your Terraform configuration, you may include the name of the current
workspace using the `${terraform.workspace}` interpolation sequence. This can
be used anywhere interpolations are allowed.
Referencing the current workspace is useful for changing behavior based
on the workspace. For example, for non-default workspaces, it may be useful
to spin up smaller cluster sizes. For example:
```hcl
resource "aws_instance" "example" {
count = "${terraform.workspace == "default" ? 5 : 1}"
# ... other arguments
}
```
Another popular use case is using the workspace name as part of naming or
tagging behavior:
```hcl
resource "aws_instance" "example" {
tags {
Name = "web - ${terraform.workspace}"
}
# ... other arguments
}
```
## Best Practices
Workspaces can be used to manage small differences between development,
staging, and production, but they **should not** be treated as the only
isolation mechanism. As Terraform configurations get larger, it's much more
manageable and safer to split one large configuration into many
smaller ones linked together with the `terraform_remote_state` data source.
This allows teams to delegate ownership and reduce the potential impact of
changes. For *each* smaller configuration, you can use workspaces to model
the differences between development, staging, and production. However, if you
have one large Terraform configuration, it is riskier and not recommended to
use workspaces to handle those differences.
[The `terraform_remote_state` data source](/docs/providers/terraform/d/remote_state.html)
accepts a `workspace` name to target. Therefore, you can link
together multiple independently managed Terraform configurations with the same
environment easily, with each configuration itself having multiple workspaces.
While workspaces are available to all,
[Terraform Enterprise](https://www.hashicorp.com/products/terraform/)
provides an interface and API for managing sets of configurations linked
with `terraform_remote_state` and viewing them all as a single environment.
Workspaces alone are useful for isolating a set of resources to test
changes during development. For example, it is common to associate a
branch in a VCS with a temporary workspace so new features can be developed
without affecting the default workspace.
Future Terraform versions and workspace enhancements will enable
Terraform to track VCS branches with a workspace to help verify only certain
branches can make changes to a Terraform workspace.
## Workspace Internals
Workspaces are technically equivalent to renaming your state file. They
aren't any more complex than that. Terraform wraps this simple notion with
a set of protections and support for remote state.
For local state, Terraform stores the workspace states in a directory called
`terraform.tfstate.d`. This directory should be be treated similarly to
local-only `terraform.tfstate`); some teams commit these files to version
control, although using a remote backend instead is recommended when there are
multiple collaborators.
For [remote state](/docs/state/remote.html), the workspaces are stored
directly in the configured [backend](/docs/backends). For example, if you
use [Consul](/docs/backends/types/consul.html), the workspaces are stored
by appending the environment name to the state path. To ensure that
workspace names are stored correctly and safely in all backends, the name
must be valid to use in a URL path segment without escaping.
The important thing about workspace internals is that workspaces are
meant to be a shared resource. They aren't a private, local-only notion
(unless you're using purely local state and not committing it).
The "current workspace" name is stored only locally in the ignored
`.terraform` directory. This allows multiple team members to work on
different workspaces concurrently.

View File

@ -1,38 +0,0 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/commands/index.html">All Providers</a>
</li>
<li<%= sidebar_current("docs-env-index") %>>
<a href="/docs/commands/env/index.html">env Command</a>
</li>
<li<%= sidebar_current("docs-env-sub") %>>
<a href="#">Subcommands</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-env-sub-list") %>>
<a href="/docs/commands/env/list.html">list</a>
</li>
<li<%= sidebar_current("docs-env-sub-select") %>>
<a href="/docs/commands/env/select.html">select</a>
</li>
<li<%= sidebar_current("docs-env-sub-new") %>>
<a href="/docs/commands/env/new.html">new</a>
</li>
<li<%= sidebar_current("docs-env-sub-delete") %>>
<a href="/docs/commands/env/delete.html">delete</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>

View File

@ -0,0 +1,38 @@
<% wrap_layout :inner do %>
<% content_for :sidebar do %>
<div class="docs-sidebar hidden-print affix-top" role="complementary">
<ul class="nav docs-sidenav">
<li<%= sidebar_current("docs-home") %>>
<a href="/docs/commands/index.html">All Commands</a>
</li>
<li<%= sidebar_current("docs-workspace-index") %>>
<a href="/docs/commands/env/index.html">workspace Command</a>
</li>
<li<%= sidebar_current("docs-workspace-sub") %>>
<a href="#">Subcommands</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-workspace-sub-list") %>>
<a href="/docs/commands/workspace/list.html">list</a>
</li>
<li<%= sidebar_current("docs-workspace-sub-select") %>>
<a href="/docs/commands/workspace/select.html">select</a>
</li>
<li<%= sidebar_current("docs-workspace-sub-new") %>>
<a href="/docs/commands/workspace/new.html">new</a>
</li>
<li<%= sidebar_current("docs-workspace-sub-delete") %>>
<a href="/docs/commands/workspace/delete.html">delete</a>
</li>
</ul>
</li>
</ul>
</div>
<% end %>
<%= yield %>
<% end %>

View File

@ -74,7 +74,7 @@
</li>
<li<%= sidebar_current("docs-commands-env") %>>
<a href="/docs/commands/env/index.html">env</a>
<a href="/docs/commands/env.html">env</a>
</li>
<li<%= sidebar_current("docs-commands-fmt") %>>
@ -140,6 +140,10 @@
<li<%= sidebar_current("docs-commands-untaint") %>>
<a href="/docs/commands/untaint.html">untaint</a>
</li>
<li<%= sidebar_current("docs-commands-workspace") %>>
<a href="/docs/commands/workspace/index.html">workspace</a>
</li>
</ul>
</li>
@ -171,8 +175,8 @@
<a href="/docs/state/locking.html">Locking</a>
</li>
<li<%= sidebar_current("docs-state-env") %>>
<a href="/docs/state/environments.html">Environments</a>
<li<%= sidebar_current("docs-state-workspaces") %>>
<a href="/docs/state/workspaces.html">Workspaces</a>
</li>
<li<%= sidebar_current("docs-state-remote") %>>