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
// of provider dependencies.
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 {
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
return 1

View File

@ -341,7 +341,12 @@ const (
// contextOpts returns the options to use to initialize a Terraform
// 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
opts.Hooks = []terraform.Hook{m.uiHook()}
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
@ -379,10 +384,10 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
}
opts.Meta = &terraform.ContextMeta{
Env: m.Workspace(),
Env: workspace,
}
return &opts
return &opts, nil
}
// 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`.
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
// to the desired named state.
func (m *Meta) Workspace() string {
current, _ := m.WorkspaceOverridden()
return current
func (m *Meta) Workspace() (string, error) {
current, overridden := m.WorkspaceOverridden()
if overridden && !validWorkspaceName(current) {
return "", invalidWorkspaceNameEnvVar
}
return current, nil
}
// 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.
cliOpts := m.backendCLIOpts()
cliOpts, err := m.backendCLIOpts()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
cliOpts.Validation = true
// 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.
workspace := m.Workspace()
workspace, err := m.Workspace()
if err != nil {
return err
}
// Check if any of the existing workspaces matches the selected
// 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 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 {
diags = diags.Append(fmt.Errorf(
"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
// 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)
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
local := backendLocal.NewWithBackend(b)
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
// 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{
CLI: m.Ui,
CLIColor: m.Colorize(),
@ -291,10 +310,10 @@ func (m *Meta) backendCLIOpts() *backend.CLIOpts {
StatePath: m.statePath,
StateOutPath: m.stateOutPath,
StateBackupPath: m.backupPath,
ContextOpts: m.contextOpts(),
ContextOpts: contextOpts,
Input: m.Input(),
RunningInAutomation: m.RunningInAutomation,
}
}, nil
}
// 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.
func (m *Meta) Operation(b backend.Backend) *backend.Operation {
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)
if err != nil {
// 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 {
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
if !migrate {
@ -261,9 +264,12 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
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
// 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 {
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
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")
}
}

View File

@ -170,7 +170,10 @@ func TestMeta_Env(t *testing.T) {
m := new(Meta)
env := m.Workspace()
env, err := m.Workspace()
if err != nil {
t.Fatal(err)
}
if env != backend.DefaultStateName {
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)
}
env = m.Workspace()
env, _ = m.Workspace()
if env != testEnv {
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)
}
env = m.Workspace()
env, _ = m.Workspace()
if env != backend.DefaultStateName {
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) {
test = false
defer func() { test = true }()

View File

@ -64,7 +64,11 @@ func (c *OutputCommand) Run(args []string) int {
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
stateStore, err := b.StateMgr(env)

View File

@ -135,7 +135,12 @@ func (c *PlanCommand) Run(args []string) int {
}
var backendForPlan plans.Backend
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
// 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
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)
if err != nil {
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 {
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)
if stateErr != nil {
c.Ui.Error(stateErr.Error())

View File

@ -41,7 +41,11 @@ func (c *StateListCommand) Run(args []string) int {
}
// 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)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))

View File

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

View File

@ -32,7 +32,11 @@ func (c *StatePullCommand) Run(args []string) int {
}
// 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)
if err != nil {
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
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)
if err != nil {
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()
// 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)
if err != nil {
c.Ui.Error(fmt.Sprintf(errStateLoadingState, err))

View File

@ -71,7 +71,11 @@ func (c *TaintCommand) Run(args []string) int {
}
// 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)
if err != nil {
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
}
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)
if err != nil {
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
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)
if err != nil {
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.Variables = varValues

View File

@ -27,7 +27,7 @@ func TestWorkspace_createAndChange(t *testing.T) {
newCmd := &WorkspaceNewCommand{}
current := newCmd.Workspace()
current, _ := newCmd.Workspace()
if current != backend.DefaultStateName {
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)
}
current = newCmd.Workspace()
current, _ = newCmd.Workspace()
if current != "test" {
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)
}
current = newCmd.Workspace()
current, _ = newCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatal("current workspace should be 'default'")
}
@ -307,7 +307,7 @@ func TestWorkspace_delete(t *testing.T) {
Meta: Meta{Ui: ui},
}
current := delCmd.Workspace()
current, _ := delCmd.Workspace()
if current != "test" {
t.Fatal("wrong workspace:", current)
}
@ -330,7 +330,7 @@ func TestWorkspace_delete(t *testing.T) {
t.Fatalf("error deleting workspace: %s", ui.ErrorWriter)
}
current = delCmd.Workspace()
current, _ = delCmd.Workspace()
if current != backend.DefaultStateName {
t.Fatalf("wrong workspace: %q", current)
}

View File

@ -91,7 +91,12 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int {
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))
return 1
}

View File

@ -20,7 +20,11 @@ func (c *WorkspaceShowCommand) Run(args []string) int {
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)
return 0