command: Always validate workspace name

The workspace name can be overridden by setting a TF_WORKSPACE
environment variable. If this is done, we should still validate the
resulting workspace name; otherwise, we could end up with an invalid and
unselectable workspace.

This change updates the Meta.Workspace function to return an error, and
handles that error wherever necessary.
This commit is contained in:
Alisdair McDiarmid 2020-06-16 12:23:15 -04:00
parent 31f858e1bb
commit b239570abb
22 changed files with 194 additions and 41 deletions

View File

@ -264,7 +264,12 @@ func (c *InitCommand) Run(args []string) int {
// on a previous run) we'll use the current state as a potential source // on a previous run) we'll use the current state as a potential source
// of provider dependencies. // of provider dependencies.
if back != nil { if back != nil {
sMgr, err := back.StateMgr(c.Workspace()) workspace, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
sMgr, err := back.StateMgr(workspace)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
return 1 return 1

View File

@ -341,7 +341,12 @@ const (
// contextOpts returns the options to use to initialize a Terraform // contextOpts returns the options to use to initialize a Terraform
// context with the settings from this Meta. // context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts { func (m *Meta) contextOpts() (*terraform.ContextOpts, error) {
workspace, err := m.Workspace()
if err != nil {
return nil, err
}
var opts terraform.ContextOpts var opts terraform.ContextOpts
opts.Hooks = []terraform.Hook{m.uiHook()} opts.Hooks = []terraform.Hook{m.uiHook()}
opts.Hooks = append(opts.Hooks, m.ExtraHooks...) opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
@ -379,10 +384,10 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
} }
opts.Meta = &terraform.ContextMeta{ opts.Meta = &terraform.ContextMeta{
Env: m.Workspace(), Env: workspace,
} }
return &opts return &opts, nil
} }
// defaultFlagSet creates a default flag set for commands. // defaultFlagSet creates a default flag set for commands.
@ -599,11 +604,16 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
// and `terraform workspace delete`. // and `terraform workspace delete`.
const WorkspaceNameEnvVar = "TF_WORKSPACE" const WorkspaceNameEnvVar = "TF_WORKSPACE"
var invalidWorkspaceNameEnvVar = fmt.Errorf("Invalid workspace name set using %s", WorkspaceNameEnvVar)
// Workspace returns the name of the currently configured workspace, corresponding // Workspace returns the name of the currently configured workspace, corresponding
// to the desired named state. // to the desired named state.
func (m *Meta) Workspace() string { func (m *Meta) Workspace() (string, error) {
current, _ := m.WorkspaceOverridden() current, overridden := m.WorkspaceOverridden()
return current if overridden && !validWorkspaceName(current) {
return "", invalidWorkspaceNameEnvVar
}
return current, nil
} }
// WorkspaceOverridden returns the name of the currently configured workspace, // WorkspaceOverridden returns the name of the currently configured workspace,

View File

@ -101,7 +101,11 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics
} }
// Setup the CLI opts we pass into backends that support it. // Setup the CLI opts we pass into backends that support it.
cliOpts := m.backendCLIOpts() cliOpts, err := m.backendCLIOpts()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
cliOpts.Validation = true cliOpts.Validation = true
// If the backend supports CLI initialization, do it. // If the backend supports CLI initialization, do it.
@ -180,7 +184,10 @@ func (m *Meta) selectWorkspace(b backend.Backend) error {
} }
// Get the currently selected workspace. // Get the currently selected workspace.
workspace := m.Workspace() workspace, err := m.Workspace()
if err != nil {
return err
}
// Check if any of the existing workspaces matches the selected // Check if any of the existing workspaces matches the selected
// workspace and create a numbered list of existing workspaces. // workspace and create a numbered list of existing workspaces.
@ -249,7 +256,11 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags
// If the backend supports CLI initialization, do it. // If the backend supports CLI initialization, do it.
if cli, ok := b.(backend.CLI); ok { if cli, ok := b.(backend.CLI); ok {
cliOpts := m.backendCLIOpts() cliOpts, err := m.backendCLIOpts()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
if err := cli.CLIInit(cliOpts); err != nil { if err := cli.CLIInit(cliOpts); err != nil {
diags = diags.Append(fmt.Errorf( diags = diags.Append(fmt.Errorf(
"Error initializing backend %T: %s\n\n"+ "Error initializing backend %T: %s\n\n"+
@ -270,7 +281,11 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags
// Otherwise, we'll wrap our state-only remote backend in the local backend // Otherwise, we'll wrap our state-only remote backend in the local backend
// to cause any operations to be run locally. // to cause any operations to be run locally.
log.Printf("[TRACE] Meta.Backend: backend %T does not support operations, so wrapping it in a local backend", b) log.Printf("[TRACE] Meta.Backend: backend %T does not support operations, so wrapping it in a local backend", b)
cliOpts := m.backendCLIOpts() cliOpts, err := m.backendCLIOpts()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
cliOpts.Validation = false // don't validate here in case config contains file(...) calls where the file doesn't exist cliOpts.Validation = false // don't validate here in case config contains file(...) calls where the file doesn't exist
local := backendLocal.NewWithBackend(b) local := backendLocal.NewWithBackend(b)
if err := local.CLIInit(cliOpts); err != nil { if err := local.CLIInit(cliOpts); err != nil {
@ -283,7 +298,11 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags
// backendCLIOpts returns a backend.CLIOpts object that should be passed to // backendCLIOpts returns a backend.CLIOpts object that should be passed to
// a backend that supports local CLI operations. // a backend that supports local CLI operations.
func (m *Meta) backendCLIOpts() *backend.CLIOpts { func (m *Meta) backendCLIOpts() (*backend.CLIOpts, error) {
contextOpts, err := m.contextOpts()
if err != nil {
return nil, err
}
return &backend.CLIOpts{ return &backend.CLIOpts{
CLI: m.Ui, CLI: m.Ui,
CLIColor: m.Colorize(), CLIColor: m.Colorize(),
@ -291,10 +310,10 @@ func (m *Meta) backendCLIOpts() *backend.CLIOpts {
StatePath: m.statePath, StatePath: m.statePath,
StateOutPath: m.stateOutPath, StateOutPath: m.stateOutPath,
StateBackupPath: m.backupPath, StateBackupPath: m.backupPath,
ContextOpts: m.contextOpts(), ContextOpts: contextOpts,
Input: m.Input(), Input: m.Input(),
RunningInAutomation: m.RunningInAutomation, RunningInAutomation: m.RunningInAutomation,
} }, nil
} }
// IsLocalBackend returns true if the backend is a local backend. We use this // IsLocalBackend returns true if the backend is a local backend. We use this
@ -318,7 +337,13 @@ func (m *Meta) IsLocalBackend(b backend.Backend) bool {
// be called. // be called.
func (m *Meta) Operation(b backend.Backend) *backend.Operation { func (m *Meta) Operation(b backend.Backend) *backend.Operation {
schema := b.ConfigSchema() schema := b.ConfigSchema()
workspace := m.Workspace() workspace, err := m.Workspace()
if err != nil {
// An invalid workspace error would have been raised when creating the
// backend, and the caller should have already exited. Seeing the error
// here first is a bug, so panic.
panic(fmt.Sprintf("invalid workspace: %s", err))
}
planOutBackend, err := m.backendState.ForPlan(schema, workspace) planOutBackend, err := m.backendState.ForPlan(schema, workspace)
if err != nil { if err != nil {
// Always indicates an implementation error in practice, because // Always indicates an implementation error in practice, because

View File

@ -180,7 +180,10 @@ func (m *Meta) backendMigrateState_S_S(opts *backendMigrateOpts) error {
func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState_S_s(opts *backendMigrateOpts) error {
log.Printf("[TRACE] backendMigrateState: target backend type %q does not support named workspaces", opts.TwoType) log.Printf("[TRACE] backendMigrateState: target backend type %q does not support named workspaces", opts.TwoType)
currentEnv := m.Workspace() currentEnv, err := m.Workspace()
if err != nil {
return err
}
migrate := opts.force migrate := opts.force
if !migrate { if !migrate {
@ -261,9 +264,12 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
return nil, err return nil, err
} }
// Ignore invalid workspace name as it is irrelevant in this context.
workspace, _ := m.Workspace()
// If the currently selected workspace is the default workspace, then set // If the currently selected workspace is the default workspace, then set
// the named workspace as the new selected workspace. // the named workspace as the new selected workspace.
if m.Workspace() == backend.DefaultStateName { if workspace == backend.DefaultStateName {
if err := m.SetWorkspace(opts.twoEnv); err != nil { if err := m.SetWorkspace(opts.twoEnv); err != nil {
return nil, fmt.Errorf("Failed to set new workspace: %s", err) return nil, fmt.Errorf("Failed to set new workspace: %s", err)
} }

View File

@ -1002,7 +1002,11 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) {
} }
// Verify we are now in the default env, or we may not be able to access the new backend // Verify we are now in the default env, or we may not be able to access the new backend
if env := m.Workspace(); env != backend.DefaultStateName { env, err := m.Workspace()
if err != nil {
t.Fatal(err)
}
if env != backend.DefaultStateName {
t.Fatal("using non-default env with single-env backend") t.Fatal("using non-default env with single-env backend")
} }
} }

View File

@ -170,7 +170,10 @@ func TestMeta_Env(t *testing.T) {
m := new(Meta) m := new(Meta)
env := m.Workspace() env, err := m.Workspace()
if err != nil {
t.Fatal(err)
}
if env != backend.DefaultStateName { if env != backend.DefaultStateName {
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env) t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
@ -181,7 +184,7 @@ func TestMeta_Env(t *testing.T) {
t.Fatal("error setting env:", err) t.Fatal("error setting env:", err)
} }
env = m.Workspace() env, _ = m.Workspace()
if env != testEnv { if env != testEnv {
t.Fatalf("expected env %q, got env %q", testEnv, env) t.Fatalf("expected env %q, got env %q", testEnv, env)
} }
@ -190,12 +193,51 @@ func TestMeta_Env(t *testing.T) {
t.Fatal("error setting env:", err) t.Fatal("error setting env:", err)
} }
env = m.Workspace() env, _ = m.Workspace()
if env != backend.DefaultStateName { if env != backend.DefaultStateName {
t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env) t.Fatalf("expected env %q, got env %q", backend.DefaultStateName, env)
} }
} }
func TestMeta_Workspace_override(t *testing.T) {
defer func(value string) {
os.Setenv(WorkspaceNameEnvVar, value)
}(os.Getenv(WorkspaceNameEnvVar))
m := new(Meta)
testCases := map[string]struct {
workspace string
err error
}{
"": {
"default",
nil,
},
"development": {
"development",
nil,
},
"invalid name": {
"",
invalidWorkspaceNameEnvVar,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
os.Setenv(WorkspaceNameEnvVar, name)
workspace, err := m.Workspace()
if workspace != tc.workspace {
t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", workspace, tc.workspace)
}
if err != tc.err {
t.Errorf("Unexpected error\n got: %s\nwant: %s\n", err, tc.err)
}
})
}
}
func TestMeta_process(t *testing.T) { func TestMeta_process(t *testing.T) {
test = false test = false
defer func() { test = true }() defer func() { test = true }()

View File

@ -64,7 +64,11 @@ func (c *OutputCommand) Run(args []string) int {
return 1 return 1
} }
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
// Get the state // Get the state
stateStore, err := b.StateMgr(env) stateStore, err := b.StateMgr(env)

View File

@ -135,7 +135,12 @@ func (c *PlanCommand) Run(args []string) int {
} }
var backendForPlan plans.Backend var backendForPlan plans.Backend
backendForPlan.Type = backendPseudoState.Type backendForPlan.Type = backendPseudoState.Type
backendForPlan.Workspace = c.Workspace() workspace, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
backendForPlan.Workspace = workspace
// Configuration is a little more awkward to handle here because it's // Configuration is a little more awkward to handle here because it's
// stored in state as raw JSON but we need it as a plans.DynamicValue // stored in state as raw JSON but we need it as a plans.DynamicValue

View File

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

View File

@ -130,7 +130,11 @@ func (c *ShowCommand) Run(args []string) int {
} }
} }
} else { } else {
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateFile, stateErr = getStateFromEnv(b, env) stateFile, stateErr = getStateFromEnv(b, env)
if stateErr != nil { if stateErr != nil {
c.Ui.Error(stateErr.Error()) c.Ui.Error(stateErr.Error())

View File

@ -41,7 +41,11 @@ func (c *StateListCommand) Run(args []string) int {
} }
// Get the state // Get the state
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))

View File

@ -37,7 +37,10 @@ func (c *StateMeta) State() (statemgr.Full, error) {
return nil, backendDiags.Err() return nil, backendDiags.Err()
} }
workspace := c.Workspace() workspace, err := c.Workspace()
if err != nil {
return nil, err
}
// Get the state // Get the state
s, err := b.StateMgr(workspace) s, err := b.StateMgr(workspace)
if err != nil { if err != nil {

View File

@ -32,7 +32,11 @@ func (c *StatePullCommand) Run(args []string) int {
} }
// Get the state manager for the current workspace // Get the state manager for the current workspace
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))

View File

@ -72,7 +72,11 @@ func (c *StatePushCommand) Run(args []string) int {
} }
// Get the state manager for the currently-selected workspace // Get the state manager for the currently-selected workspace
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load destination state: %s", err))

View File

@ -89,7 +89,11 @@ func (c *StateShowCommand) Run(args []string) int {
schemas := ctx.Schemas() schemas := ctx.Schemas()
// Get the state // Get the state
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))

View File

@ -71,7 +71,11 @@ func (c *TaintCommand) Run(args []string) int {
} }
// Get the state // Get the state
env := c.Workspace() env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env) stateMgr, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

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

View File

@ -66,7 +66,11 @@ func (c *UntaintCommand) Run(args []string) int {
} }
// Get the state // Get the state
workspace := c.Workspace() workspace, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(workspace) stateMgr, err := b.StateMgr(workspace)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))

View File

@ -110,7 +110,11 @@ func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
} }
} }
opts := c.contextOpts() opts, err := c.contextOpts()
if err != nil {
diags = diags.Append(err)
return diags
}
opts.Config = cfg opts.Config = cfg
opts.Variables = varValues opts.Variables = varValues

View File

@ -27,7 +27,7 @@ func TestWorkspace_createAndChange(t *testing.T) {
newCmd := &WorkspaceNewCommand{} newCmd := &WorkspaceNewCommand{}
current := newCmd.Workspace() current, _ := newCmd.Workspace()
if current != backend.DefaultStateName { if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'") t.Fatal("current workspace should be 'default'")
} }
@ -39,7 +39,7 @@ func TestWorkspace_createAndChange(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
} }
current = newCmd.Workspace() current, _ = newCmd.Workspace()
if current != "test" { if current != "test" {
t.Fatalf("current workspace should be 'test', got %q", current) t.Fatalf("current workspace should be 'test', got %q", current)
} }
@ -52,7 +52,7 @@ func TestWorkspace_createAndChange(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter)
} }
current = newCmd.Workspace() current, _ = newCmd.Workspace()
if current != backend.DefaultStateName { if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'") t.Fatal("current workspace should be 'default'")
} }
@ -307,7 +307,7 @@ func TestWorkspace_delete(t *testing.T) {
Meta: Meta{Ui: ui}, Meta: Meta{Ui: ui},
} }
current := delCmd.Workspace() current, _ := delCmd.Workspace()
if current != "test" { if current != "test" {
t.Fatal("wrong workspace:", current) t.Fatal("wrong workspace:", current)
} }
@ -330,7 +330,7 @@ func TestWorkspace_delete(t *testing.T) {
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter) t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
} }
current = delCmd.Workspace() current, _ = delCmd.Workspace()
if current != backend.DefaultStateName { if current != backend.DefaultStateName {
t.Fatalf("wrong workspace: %q", current) t.Fatalf("wrong workspace: %q", current)
} }

View File

@ -91,7 +91,12 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int {
return 1 return 1
} }
if workspace == c.Workspace() { currentWorkspace, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
if workspace == currentWorkspace {
c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envDelCurrent), workspace)) c.Ui.Error(fmt.Sprintf(strings.TrimSpace(envDelCurrent), workspace))
return 1 return 1
} }

View File

@ -20,7 +20,11 @@ func (c *WorkspaceShowCommand) Run(args []string) int {
return 1 return 1
} }
workspace := c.Workspace() workspace, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
c.Ui.Output(workspace) c.Ui.Output(workspace)
return 0 return 0