Skip to content

Prevent creating stack with name of existing stack#1969

Open
jordanstephens wants to merge 3 commits intomainfrom
jordan/warn-existing-stack
Open

Prevent creating stack with name of existing stack#1969
jordanstephens wants to merge 3 commits intomainfrom
jordan/warn-existing-stack

Conversation

@jordanstephens
Copy link
Member

@jordanstephens jordanstephens commented Mar 6, 2026

Description

Fail to create a stack if the name is invalid. Check if passed by arg or by interactive survey

Linked Issues

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • New Features

    • Stack creation now validates stack names to prevent duplicates within your project. Stack names must contain only alphanumeric characters and begin with a letter. Clear error messages guide users when duplicate names or invalid formats are detected.
  • Tests

    • Added comprehensive test coverage for stack name validation, duplicate detection, and error handling scenarios.

@jordanstephens jordanstephens requested a review from lionello as a code owner March 6, 2026 22:34
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

The PR adds duplicate stack name validation to prevent creating stacks with identical names in a project. It introduces a public ValidateStackName() utility function, refactors stack name validation across multiple layers (wizard, stacks package, CLI command), and adds a stackExists() helper in the CLI that queries existing stacks via the Fabric client's ListStacks RPC before stack creation.

Changes

Cohort / File(s) Summary
Stack Existence Validation (CLI)
src/cmd/cli/command/stack.go, stack_test.go
Added stackExists() helper to query existing stacks via ListStacks RPC; integrated existence checks in stack creation flow before proceeding; imported defangv1 client package; expanded tests with mock client and test cases covering duplicate scenarios and error propagation.
Stack Name Validation Utilities
src/pkg/stacks/stacks.go, stacks_test.go, wizard.go
Introduced public ValidateStackName() function and exposed StackNamePattern for validation; refactored CreateInDirectory, RemoveInDirectory, and ValidStackName to delegate to centralized validation; updated tests and error messages to reflect new validation path.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI Command
    participant Validator as Stack Validator
    participant Client as Fabric Client
    participant Project as Project State

    User->>CLI: Create stack (provide name)
    CLI->>Validator: ValidateStackName(name)
    Validator-->>CLI: Validation result
    
    alt Name is invalid
        CLI-->>User: Error: Invalid stack name
    else Name is valid
        CLI->>Client: ListStacks(project)
        Client->>Project: Query existing stacks
        Project-->>Client: Return stacks list
        Client-->>CLI: Stacks found
        CLI->>Validator: stackExists(name, stacks)
        Validator-->>CLI: Existence check result
        
        alt Stack exists
            CLI-->>User: Error: Stack already exists
        else Stack does not exist
            CLI->>Project: Create new stack
            Project-->>CLI: Success
            CLI-->>User: Stack created
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Use dedicated Stack RPCs #1780 — Introduces the ListStacks RPC and defangv1 protobuf types that this PR's stackExists() helper directly depends on.
  • Lio/fix default stack #1820 — Modifies the CLI stack flow and Fabric client interactions (LoadProjectName, ListStacks calls) that overlap with stack creation plumbing.
  • elicitations validations #1915 — Touches stack-name validation utilities (ValidStackName, ValidateStackName) that are refactored in this PR's stacks package changes.

Suggested reviewers

  • lionello
  • raphaeltm

Poem

🐰 A stack by any other name, would not be the same,
We check before we create, to prevent the duplicate game,
ListStacks queries the project with care,
Validation ensures no duplicates there!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: preventing stack creation with duplicate names, which matches the primary change throughout the codebase.
Linked Issues check ✅ Passed The PR implements the core coding requirement from issue #594: detecting and preventing creation of stacks with existing names via stackExists() validation in CLI and tests.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objective: stack existence validation, refactored validation utility, and related test coverage for CLI and helper functions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jordan/warn-existing-stack

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

level=warning msg="[linters_context] running gomodguard failed: unable to read module file go.mod: current working directory must have a go.mod file: if you are not using go modules it is suggested to disable this linter"
level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@jordanstephens jordanstephens changed the title Jordan/warn existing stack Prevent creating stack with name of existing stack Mar 6, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cmd/cli/command/stack_test.go (1)

165-184: ⚠️ Potential issue | 🟡 Minor

Two table cases aren’t exercising the paths they’re named after.

"missing stack name" calls RunE with one empty arg, which the CLI can’t actually produce, and "stack already exists" uses existing-stack, which fails name validation before stackExists() runs. That leaves both the zero-arg path and the duplicate-name path untested.

💡 Suggested change
 		{
 			name: "missing stack name",
 			parameters: stacks.Parameters{
 				Name:     "",
@@
 		{
 			name: "stack already exists",
 			parameters: stacks.Parameters{
-				Name:     "existing-stack",
+				Name:     "existingstack",
 				Provider: client.ProviderAWS,
 				Region:   "us-test-2",
 				Mode:     modes.ModeAffordable,
 			},
-			existingStacks: []*defangv1.Stack{{Name: "existing-stack", Project: ""}},
+			existingStacks: []*defangv1.Stack{{Name: "existingstack", Project: ""}},
 			expectErr:      true,
 		},
@@
-			err := stackCreateCmd.RunE(stackCreateCmd, []string{tt.parameters.Name})
+			var args []string
+			if tt.parameters.Name != "" {
+				args = []string{tt.parameters.Name}
+			}
+			err := stackCreateCmd.RunE(stackCreateCmd, args)

Also applies to: 191-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/cli/command/stack_test.go` around lines 165 - 184, The two test cases
in stack_test.go are not exercising the intended paths: change the "missing
stack name" case to call the command's RunE with zero args (simulate CLI
invocation with no arguments) so the name-validation path for empty input is
hit, and change the "stack already exists" case to use a valid, non-empty stack
name that passes name validation (e.g., "existing-stack" kept but ensure the
test supplies it as a proper argument) so that stackExists() is reached and
triggers the duplicate-name error; update the test invocations around
RunE/Execute/command creation in the same file to reflect zero-arg invocation
for the first case and valid-arg invocation for the second so the code paths for
name missing and duplicate name are both covered.
🧹 Nitpick comments (1)
src/pkg/stacks/stacks_test.go (1)

30-31: Use ValidateStackName() here instead of the raw regexp.

This test is checking whether MakeDefaultName() produces a name the package accepts. Matching StackNamePattern directly couples the assertion to the current implementation and will miss any extra rules added in ValidateStackName().

💡 Suggested change
-			if !StackNamePattern.MatchString(result) {
-				t.Errorf("MakeDefaultName() produced invalid stack name: %q", result)
-			}
+			if err := ValidateStackName(result); err != nil {
+				t.Errorf("MakeDefaultName() produced invalid stack name %q: %v", result, err)
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/stacks/stacks_test.go` around lines 30 - 31, The test currently
asserts MakeDefaultName() by matching against StackNamePattern directly; change
it to call ValidateStackName(result) and assert that it returns nil (or no
error) so the test verifies the full validation logic. Replace the direct
StackNamePattern.MatchString(result) check with a call to
ValidateStackName(result) and update the failure message to include the returned
error (e.g., "MakeDefaultName() produced invalid stack name: %q: %v") so the
test fails with the validator's reason.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cmd/cli/command/stack_test.go`:
- Around line 25-29: The mock ListStacks implementation
(mockFabricClientWithStacks.ListStacks) ignores ListStacksRequest.Project, so
tests like TestStackExists won't catch code that fails to scope by project;
modify ListStacks to check the incoming request.Project against an expected
project value stored on the mock (e.g., add a field like expectedProject or use
m.project) and return an error (or empty result) when they differ, while still
returning m.listStacksErr when set and returning the stacked response when the
project matches.

In `@src/cmd/cli/command/stack.go`:
- Around line 47-49: The validation call to stacks.ValidateStackName should only
run when the user actually supplied the positional STACK_NAME; currently it runs
unconditionally and rejects the zero-arg interactive flow when stackName == "".
Update the branch around ValidateStackName (the code referencing stackName and
stacks.ValidateStackName) to first check that stackName is non-empty (or that
the positional arg count > 0) and only then call stacks.ValidateStackName,
leaving the empty stackName path to proceed to the interactive survey.

In `@src/pkg/stacks/stacks_test.go`:
- Around line 202-205: The test asserts a platform-specific error string after
calling RemoveInDirectory(".", "nonexistingstack"); instead, check for the
standard not-found sentinel: replace assert.ErrorContains(t, err, "...no such
file...") with a portability-safe assertion like assert.ErrorIs(t, err,
os.ErrNotExist) (or assert.True(t, errors.Is(err, os.ErrNotExist)) if ErrorIs
isn't available) and add the necessary import for os (and errors if used); keep
the existing assert.Error(t, err) or remove it since ErrorIs covers presence.

---

Outside diff comments:
In `@src/cmd/cli/command/stack_test.go`:
- Around line 165-184: The two test cases in stack_test.go are not exercising
the intended paths: change the "missing stack name" case to call the command's
RunE with zero args (simulate CLI invocation with no arguments) so the
name-validation path for empty input is hit, and change the "stack already
exists" case to use a valid, non-empty stack name that passes name validation
(e.g., "existing-stack" kept but ensure the test supplies it as a proper
argument) so that stackExists() is reached and triggers the duplicate-name
error; update the test invocations around RunE/Execute/command creation in the
same file to reflect zero-arg invocation for the first case and valid-arg
invocation for the second so the code paths for name missing and duplicate name
are both covered.

---

Nitpick comments:
In `@src/pkg/stacks/stacks_test.go`:
- Around line 30-31: The test currently asserts MakeDefaultName() by matching
against StackNamePattern directly; change it to call ValidateStackName(result)
and assert that it returns nil (or no error) so the test verifies the full
validation logic. Replace the direct StackNamePattern.MatchString(result) check
with a call to ValidateStackName(result) and update the failure message to
include the returned error (e.g., "MakeDefaultName() produced invalid stack
name: %q: %v") so the test fails with the validator's reason.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61061d17-cd8e-4771-b5cd-0416519ff04b

📥 Commits

Reviewing files that changed from the base of the PR and between 70f85a4 and 3aa15be.

📒 Files selected for processing (5)
  • src/cmd/cli/command/stack.go
  • src/cmd/cli/command/stack_test.go
  • src/pkg/stacks/stacks.go
  • src/pkg/stacks/stacks_test.go
  • src/pkg/stacks/wizard.go

Comment on lines +25 to +29
func (m mockFabricClientWithStacks) ListStacks(_ context.Context, _ *defangv1.ListStacksRequest) (*defangv1.ListStacksResponse, error) {
if m.listStacksErr != nil {
return nil, m.listStacksErr
}
return &defangv1.ListStacksResponse{Stacks: m.existingStacks}, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the mock verify the requested project.

This mock ignores ListStacksRequest.Project, so TestStackExists would still pass if stackExists() stopped scoping the RPC to the current project and started rejecting names from other projects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/cli/command/stack_test.go` around lines 25 - 29, The mock ListStacks
implementation (mockFabricClientWithStacks.ListStacks) ignores
ListStacksRequest.Project, so tests like TestStackExists won't catch code that
fails to scope by project; modify ListStacks to check the incoming
request.Project against an expected project value stored on the mock (e.g., add
a field like expectedProject or use m.project) and return an error (or empty
result) when they differ, while still returning m.listStacksErr when set and
returning the stacked response when the project matches.

Comment on lines +47 to +49
if err := stacks.ValidateStackName(stackName); err != nil {
return fmt.Errorf("invalid stack name %q: %v", stackName, err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t reject the zero-arg interactive path here.

When defang stack new is run without STACK_NAME, stackName is still "" at this point, so the command now exits with invalid stack name "" instead of launching the survey. This validation needs to run only when the positional arg was actually provided.

💡 Suggested change
-			if err := stacks.ValidateStackName(stackName); err != nil {
-				return fmt.Errorf("invalid stack name %q: %v", stackName, err)
-			}
+			if stackName != "" {
+				if err := stacks.ValidateStackName(stackName); err != nil {
+					return fmt.Errorf("invalid stack name %q: %v", stackName, err)
+				}
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := stacks.ValidateStackName(stackName); err != nil {
return fmt.Errorf("invalid stack name %q: %v", stackName, err)
}
if stackName != "" {
if err := stacks.ValidateStackName(stackName); err != nil {
return fmt.Errorf("invalid stack name %q: %v", stackName, err)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cmd/cli/command/stack.go` around lines 47 - 49, The validation call to
stacks.ValidateStackName should only run when the user actually supplied the
positional STACK_NAME; currently it runs unconditionally and rejects the
zero-arg interactive flow when stackName == "". Update the branch around
ValidateStackName (the code referencing stackName and stacks.ValidateStackName)
to first check that stackName is non-empty (or that the positional arg count >
0) and only then call stacks.ValidateStackName, leaving the empty stackName path
to proceed to the interactive survey.

Comment on lines +202 to +205
err := RemoveInDirectory(".", "nonexistingstack")
// expect an error when trying to remove a non-existing stack
assert.Error(t, err)
assert.ErrorContains(t, err, "remove .defang/non_existing_stack: no such file or directory")
assert.ErrorContains(t, err, "remove .defang/nonexistingstack: no such file or directory")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid asserting the platform-specific os.Remove text.

The exact error string varies by OS and path separator. ErrorIs(..., os.ErrNotExist) keeps the test stable while still verifying the behavior you care about.

💡 Suggested change
 		err := RemoveInDirectory(".", "nonexistingstack")
 		// expect an error when trying to remove a non-existing stack
 		assert.Error(t, err)
-		assert.ErrorContains(t, err, "remove .defang/nonexistingstack: no such file or directory")
+		assert.ErrorIs(t, err, os.ErrNotExist)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err := RemoveInDirectory(".", "nonexistingstack")
// expect an error when trying to remove a non-existing stack
assert.Error(t, err)
assert.ErrorContains(t, err, "remove .defang/non_existing_stack: no such file or directory")
assert.ErrorContains(t, err, "remove .defang/nonexistingstack: no such file or directory")
err := RemoveInDirectory(".", "nonexistingstack")
// expect an error when trying to remove a non-existing stack
assert.Error(t, err)
assert.ErrorIs(t, err, os.ErrNotExist)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pkg/stacks/stacks_test.go` around lines 202 - 205, The test asserts a
platform-specific error string after calling RemoveInDirectory(".",
"nonexistingstack"); instead, check for the standard not-found sentinel: replace
assert.ErrorContains(t, err, "...no such file...") with a portability-safe
assertion like assert.ErrorIs(t, err, os.ErrNotExist) (or assert.True(t,
errors.Is(err, os.ErrNotExist)) if ErrorIs isn't available) and add the
necessary import for os (and errors if used); keep the existing assert.Error(t,
err) or remove it since ErrorIs covers presence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant