Merge #14952: rename "state environments" to "workspaces"
This commit is contained in:
commit
698c22c924
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
// 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
|
||||
StateEnvDir 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,10 +291,19 @@ func (b *Local) init() {
|
|||
Default: "",
|
||||
},
|
||||
|
||||
"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",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ func TestLocal(t *testing.T) *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"),
|
||||
StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"),
|
||||
ContextOpts: &terraform.ContextOpts{},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}`
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"version": 1,
|
||||
"modules": [{
|
||||
"path": ["root"],
|
||||
"outputs": { "foo": "bar" }
|
||||
}]
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
)
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
)
|
|
@ -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!")
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
45
commands.go
45
commands.go
|
@ -72,32 +72,37 @@ func init() {
|
|||
},
|
||||
|
||||
"env": func() (cli.Command, error) {
|
||||
return &command.EnvCommand{
|
||||
return &command.WorkspaceCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env list": func() (cli.Command, error) {
|
||||
return &command.EnvListCommand{
|
||||
return &command.WorkspaceListCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env select": func() (cli.Command, error) {
|
||||
return &command.EnvSelectCommand{
|
||||
return &command.WorkspaceSelectCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env new": func() (cli.Command, error) {
|
||||
return &command.EnvNewCommand{
|
||||
return &command.WorkspaceNewCommand{
|
||||
Meta: meta,
|
||||
LegacyName: true,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"env delete": func() (cli.Command, error) {
|
||||
return &command.EnvDeleteCommand{
|
||||
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
|
||||
//-----------------------------------------------------------
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
|
@ -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"!
|
||||
```
|
|
@ -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.
|
|
@ -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
|
||||
```
|
|
@ -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.
|
||||
```
|
|
@ -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"!
|
||||
```
|
|
@ -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)
|
||||
|
|
|
@ -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".
|
||||
```
|
|
@ -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.
|
|
@ -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
|
||||
```
|
|
@ -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.
|
||||
```
|
|
@ -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".
|
||||
```
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
|
@ -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 %>
|
|
@ -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 %>
|
|
@ -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") %>>
|
||||
|
|
Loading…
Reference in New Issue