Skip to content

Commit 8f0c520

Browse files
ostermanclaude
andcommitted
fix: Properly differentiate workspace errors in terraform output
- Add isWorkspaceExistsError helper to check for "already exists" errors - Fail fast on unexpected errors (network, permission) instead of silently falling through to workspace select - Add tests for both "already exists" and unexpected error cases - Fix godot linter issue in comment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 48de128 commit 8f0c520

File tree

3 files changed

+65
-6
lines changed

3 files changed

+65
-6
lines changed

pkg/terraform/output/executor.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/samber/lo"
1313

1414
errUtils "github.com/cloudposse/atmos/errors"
15+
"github.com/cloudposse/atmos/pkg/auth"
1516
log "github.com/cloudposse/atmos/pkg/logger"
1617
"github.com/cloudposse/atmos/pkg/perf"
1718
"github.com/cloudposse/atmos/pkg/schema"
@@ -217,6 +218,13 @@ func (e *Executor) GetOutput(
217218
) (any, bool, error) {
218219
defer perf.Track(atmosConfig, "output.Executor.GetOutput")()
219220

221+
// Validate authManager type if provided.
222+
if authManager != nil {
223+
if _, ok := authManager.(auth.AuthManager); !ok {
224+
return nil, false, fmt.Errorf("%w: expected auth.AuthManager", errUtils.ErrInvalidAuthManagerType)
225+
}
226+
}
227+
220228
stackSlug := fmt.Sprintf("%s-%s", stack, component)
221229

222230
// Check cache first.

pkg/terraform/output/executor_test.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func TestExecutor_ExecuteWithSections_InitError(t *testing.T) {
229229
assert.True(t, errors.Is(err, errUtils.ErrTerraformInit), "expected ErrTerraformInit")
230230
}
231231

232-
func TestExecutor_ExecuteWithSections_WorkspaceNewError(t *testing.T) {
232+
func TestExecutor_ExecuteWithSections_WorkspaceNewError_AlreadyExists(t *testing.T) {
233233
ctrl := gomock.NewController(t)
234234
defer ctrl.Finish()
235235

@@ -244,17 +244,44 @@ func TestExecutor_ExecuteWithSections_WorkspaceNewError(t *testing.T) {
244244
atmosConfig := validAtmosConfig()
245245
sections := validSections()
246246

247-
// Setup expectations.
247+
// Setup expectations - workspace already exists, so it falls back to select.
248248
mockRunner.EXPECT().SetEnv(gomock.Any()).Return(nil).AnyTimes()
249249
mockRunner.EXPECT().Init(gomock.Any(), gomock.Any()).Return(nil)
250-
mockRunner.EXPECT().WorkspaceNew(gomock.Any(), "test-workspace").Return(errors.New("workspace exists"))
250+
mockRunner.EXPECT().WorkspaceNew(gomock.Any(), "test-workspace").Return(errors.New("Workspace test-workspace already exists"))
251251
mockRunner.EXPECT().WorkspaceSelect(gomock.Any(), "test-workspace").Return(errors.New("select failed"))
252252

253253
_, err := exec.ExecuteWithSections(atmosConfig, "test-component", "test-stack", sections, nil)
254254
require.Error(t, err)
255255
assert.True(t, errors.Is(err, errUtils.ErrTerraformWorkspaceOp), "expected ErrTerraformWorkspaceOp")
256256
}
257257

258+
func TestExecutor_ExecuteWithSections_WorkspaceNewError_Unexpected(t *testing.T) {
259+
ctrl := gomock.NewController(t)
260+
defer ctrl.Finish()
261+
262+
mockDescriber := NewMockComponentDescriber(ctrl)
263+
mockRunner := NewMockTerraformRunner(ctrl)
264+
265+
customFactory := func(workdir, executable string) (TerraformRunner, error) {
266+
return mockRunner, nil
267+
}
268+
269+
exec := NewExecutor(mockDescriber, WithRunnerFactory(customFactory))
270+
atmosConfig := validAtmosConfig()
271+
sections := validSections()
272+
273+
// Setup expectations - unexpected error (network, permission, etc.) should fail fast.
274+
mockRunner.EXPECT().SetEnv(gomock.Any()).Return(nil).AnyTimes()
275+
mockRunner.EXPECT().Init(gomock.Any(), gomock.Any()).Return(nil)
276+
mockRunner.EXPECT().WorkspaceNew(gomock.Any(), "test-workspace").Return(errors.New("network timeout"))
277+
// WorkspaceSelect should NOT be called for unexpected errors.
278+
279+
_, err := exec.ExecuteWithSections(atmosConfig, "test-component", "test-stack", sections, nil)
280+
require.Error(t, err)
281+
assert.True(t, errors.Is(err, errUtils.ErrTerraformWorkspaceOp), "expected ErrTerraformWorkspaceOp")
282+
assert.Contains(t, err.Error(), "network timeout")
283+
}
284+
258285
func TestExecutor_ExecuteWithSections_OutputError(t *testing.T) {
259286
ctrl := gomock.NewController(t)
260287
defer ctrl.Finish()

pkg/terraform/output/workspace.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
errUtils "github.com/cloudposse/atmos/errors"
910
log "github.com/cloudposse/atmos/pkg/logger"
@@ -68,11 +69,23 @@ func (m *defaultWorkspaceManager) EnsureWorkspace(
6869
return nil
6970
}
7071

71-
// Log the creation failure before attempting select.
72-
log.Debug("Workspace creation failed, attempting select", "workspace", workspace, "error", err)
72+
// Check if this is a "workspace already exists" error.
73+
// terraform-exec doesn't provide typed errors, so we check the error message.
74+
// Terraform CLI outputs "Workspace X already exists" when trying to create an existing workspace.
75+
if !isWorkspaceExistsError(err) {
76+
// This is an unexpected error (network, permission, etc.) - fail fast.
77+
log.Debug("Workspace creation failed with unexpected error", "workspace", workspace, "error", err)
78+
return wrapErrorWithStderr(
79+
errUtils.Build(errUtils.ErrTerraformWorkspaceOp).
80+
WithCause(err).
81+
WithExplanationf("Failed to create workspace '%s' for %s.", workspace, GetComponentInfo(component, stack)).
82+
Err(),
83+
stderrCapture,
84+
)
85+
}
7386

7487
// Workspace already exists, select it.
75-
log.Debug("Selecting existing terraform workspace", "workspace", workspace, "component", component, "stack", stack)
88+
log.Debug("Workspace already exists, selecting it", "workspace", workspace, "component", component, "stack", stack)
7689

7790
if err := runner.WorkspaceSelect(ctx, workspace); err != nil {
7891
return wrapErrorWithStderr(
@@ -89,3 +102,14 @@ func (m *defaultWorkspaceManager) EnsureWorkspace(
89102
windowsFileDelay()
90103
return nil
91104
}
105+
106+
// isWorkspaceExistsError checks if the error indicates the workspace already exists.
107+
// Terraform-exec doesn't provide typed errors, so we check the error message.
108+
// Terraform CLI outputs "Workspace X already exists" when trying to create an existing workspace.
109+
func isWorkspaceExistsError(err error) bool {
110+
if err == nil {
111+
return false
112+
}
113+
errMsg := strings.ToLower(err.Error())
114+
return strings.Contains(errMsg, "already exists")
115+
}

0 commit comments

Comments
 (0)