Skip to content

Handle already-running containers and port conflicts#40

Merged
carole-lavillonniere merged 4 commits intomainfrom
carole/drg-357
Feb 24, 2026
Merged

Handle already-running containers and port conflicts#40
carole-lavillonniere merged 4 commits intomainfrom
carole/drg-357

Conversation

@carole-lavillonniere
Copy link
Copy Markdown
Collaborator

Before pulling images or validating licenses, lstk start now checks the state of each configured container:

  • If a container is already running, it logs "<name> is already running" and skips it. If all containers are already running, the command exits successfully without doing any work.
  • If the configured port is already bound by another process, it returns an error "port <N> already in use" with a hint pointing to the config file.

@carole-lavillonniere
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0227242 and 1782cb2.

📒 Files selected for processing (7)
  • internal/container/start.go
  • internal/ports/ports.go
  • internal/runtime/runtime.go
  • test/integration/license_test.go
  • test/integration/main_test.go
  • test/integration/start_test.go
  • test/integration/stop_test.go

📝 Walkthrough

Walkthrough

Implements a staged container startup pipeline: select containers to start (skip running / check ports), pre-pull images, validate licenses per container, then start containers with readiness/health polling. Adds port-check utility and extends container config with Tag and ProductName; updates integration tests and shared test helpers.

Changes

Cohort / File(s) Summary
Container Startup Pipeline
internal/container/start.go
Replaces single-pass start with staged flow: selectContainersToStart (skip running, port checks), pullImages (pre-pull with progress), validateLicenses, and startContainers (create/start, readiness & health polling). Replaces config field uses with container-scoped fields (Tag, ProductName, Name, Image, Port, HealthPath).
Runtime API
internal/runtime/runtime.go
Adds public fields Tag string and ProductName string to runtime.ContainerConfig.
Port utility
internal/ports/ports.go
New CheckAvailable(port string) error function that TCP-dials localhost:port with timeout and returns error if port is in use.
Integration Test scaffolding
test/integration/main_test.go
Adds shared Docker test helpers, imports, and constants (containerName, testImage) plus startTestContainer helper to centralize test container lifecycle.
Integration Tests: start & license
test/integration/start_test.go, test/integration/license_test.go
Adds tests for "already running" and "port in use" scenarios; license_test.go updated to use consolidated containerName constant and adjusted inspection/cleanup.
Integration Tests: stop
test/integration/stop_test.go
Removes local startTestContainer and testImage (moved to shared helpers); removes unused imports.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Start Command
    participant Runtime as Runtime Manager
    participant Ports as Port Checker
    participant Docker as Docker Engine
    participant API as License API

    CLI->>Runtime: start(containers)
    Runtime->>Runtime: selectContainersToStart(containers)
    Runtime->>Ports: CheckAvailable(port)
    Ports-->>Runtime: available / in-use
    Runtime->>CLI: emit skipped/selected messages
    Runtime->>Docker: pullImages(selected)
    Docker-->>Runtime: pull progress/result
    Runtime->>API: validateLicenses(container ProductName/Tag)
    API-->>Runtime: license responses
    Runtime->>Docker: startContainers(selected)
    Docker-->>Runtime: start result
    Runtime->>Runtime: wait for readiness / poll HealthPath
    Runtime->>CLI: emit start/ready/health status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: handling already-running containers and port conflicts, which aligns with the primary objectives of the PR.
Description check ✅ Passed The description directly relates to the changeset, explaining the new behavior for checking container state and port availability before pulling images or validating licenses.

✏️ 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 carole/drg-357

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/container/start.go`:
- Around line 57-75: The token is currently retrieved before
selectContainersToStart which prevents early-return when no containers need
starting; move the token retrieval to after selectContainersToStart and the
len(containers)==0 check so lstk can fast-exit without failing on missing token.
Update the flow in start.go: call selectContainersToStart(...) first, if
len(containers)==0 return nil, then fetch the token (the existing token
retrieval logic), then ensure the filtered containers (the returned containers
value) are injected/associated with the token before calling pullImages(...),
validateLicenses(..., token) and startContainers(...). Adjust any code that
passes cfg.Containers or containers to validateLicenses to use the token-aware
container set.

Copy link
Copy Markdown
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

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

LGTM! Just some thoughts that popped up

return nil
}

func selectContainersToStart(ctx context.Context, rt runtime.Runtime, sink output.Sink, containers []runtime.ContainerConfig, cfgContainers []config.ContainerConfig) ([]runtime.ContainerConfig, []config.ContainerConfig, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: that's a long line. Wild that gofmt doesn't split it up :) Maybe gofumpt would?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah gofmt does not care about line length and leave it up to the coder to wrap.
So if we want it we'd need to introduce another tool.

Comment on lines +129 to +130
var filtered []runtime.ContainerConfig
var filteredCfg []config.ContainerConfig
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: I wonder if we could somehow compose those together so that we don't have to handle them separately, like having runtime.ContainerConfig contain config.ContainerConfig, somewhat like this:

type ContainerConfig struct {
	Image      string
	Name       string
	Port       string
	HealthPath string
	Env        []string // e.g., ["KEY=value", "FOO=bar"]
	*config.ContainerConfig
}

Not sure if it wouldn't make things annoying somewhere else though :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's a fair point, it looks quite complex at the moment.
That suggestion would require internal/runtime to import internal/config though. It could cause import cycle.
What about this instead? 0227242
I added Tag and ProductName to runtime.ContainerConfig

Comment on lines +153 to +163
func checkPortAvailable(port string) error {
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
if err != nil {
return nil
}
err = conn.Close()
if err != nil {
return nil
}
return fmt.Errorf("port %s already in use", port)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thought: I wonder if it makes sense to have that somewhere in a util/x, but we might only need it here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, moved it to internal/ports f4f420e

Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (1)
test/integration/start_test.go (1)

106-124: Same environment variable consistency issue; consider binding to localhost explicitly.

Same suggestion as above regarding env.With() usage.

The static analysis warning about binding to all interfaces is a false positive in this test context—you intentionally need to occupy the port. However, binding to 127.0.0.1:4566 instead of :4566 would be slightly more precise since checkPortAvailable in start.go dials localhost.

♻️ Optional: more precise binding
-	ln, err := net.Listen("tcp", ":4566")
+	ln, err := net.Listen("tcp", "127.0.0.1:4566")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/start_test.go` around lines 106 - 124, In
TestStartCommandFailsWhenPortInUse update the port binding to be explicit to
localhost by changing the net.Listen call in TestStartCommandFailsWhenPortInUse
from ":4566" to "127.0.0.1:4566" so it matches how checkPortAvailable dials
localhost; also make the environment-setting consistent by replacing the manual
append to cmd.Env (cmd.Env = append(os.Environ(),
"LOCALSTACK_AUTH_TOKEN=fake-token")) with the project’s env helper (e.g.,
env.With or equivalent used elsewhere) to keep env construction consistent
across tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/container/start.go`:
- Around line 157-167: In checkPortAvailable, when net.DialTimeout succeeds it
indicates the port is in use, so always return the "port %s already in use"
error regardless of conn.Close() result; do not return nil on a Close error.
Update the function (checkPortAvailable) to defer or call conn.Close() but
ignore or log any Close error and then return fmt.Errorf("port %s already in
use", port) after a successful DialTimeout, ensuring Dial success drives the
error path.

In `@test/integration/start_test.go`:
- Around line 88-104: The test TestStartCommandDoesNothingWhenAlreadyRunning
uses append(os.Environ(), ...) and the wrong env var name; replace that with the
project's env helper so the CLI reads the correct token: use
env.With(env.AuthToken, "fake-token") for the cmd.Env setup (the same approach
used in other tests), i.e. update the exec.CommandContext invocation in
TestStartCommandDoesNothingWhenAlreadyRunning to set cmd.Env via env.With and
reference env.AuthToken instead of "LOCALSTACK_AUTH_TOKEN" so startTestContainer
and the CLI see the correct LSTK_AUTH_TOKEN value.

---

Duplicate comments:
In `@internal/container/start.go`:
- Around line 27-30: Token is still fetched before selecting containers; change
the flow in start.go so selectContainersToStart(...) is called first and its
result examined, and only if there are containers to start call a.GetToken(ctx)
and proceed; specifically, move the GetToken call to after
selectContainersToStart and guard token retrieval and any subsequent operations
on token by checking that the returned list from selectContainersToStart is
non-empty (use the selectContainersToStart return value and the GetToken method
names to locate where to reorder).

---

Nitpick comments:
In `@test/integration/start_test.go`:
- Around line 106-124: In TestStartCommandFailsWhenPortInUse update the port
binding to be explicit to localhost by changing the net.Listen call in
TestStartCommandFailsWhenPortInUse from ":4566" to "127.0.0.1:4566" so it
matches how checkPortAvailable dials localhost; also make the
environment-setting consistent by replacing the manual append to cmd.Env
(cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=fake-token")) with the
project’s env helper (e.g., env.With or equivalent used elsewhere) to keep env
construction consistent across tests.

Comment on lines +157 to +167
func checkPortAvailable(port string) error {
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
if err != nil {
return nil
}
err = conn.Close()
if err != nil {
return nil
}
return fmt.Errorf("port %s already in use", port)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Bug: returns nil when conn.Close() fails, masking the port-in-use condition.

If DialTimeout succeeds (meaning the port is in use) but conn.Close() subsequently fails, the function incorrectly returns nil instead of the "port already in use" error. The close error is irrelevant to the port availability check.

🐛 Proposed fix
 func checkPortAvailable(port string) error {
 	conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
 	if err != nil {
 		return nil
 	}
-	err = conn.Close()
-	if err != nil {
-		return nil
-	}
+	_ = conn.Close()
 	return fmt.Errorf("port %s already in use", port)
 }
📝 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
func checkPortAvailable(port string) error {
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
if err != nil {
return nil
}
err = conn.Close()
if err != nil {
return nil
}
return fmt.Errorf("port %s already in use", port)
}
func checkPortAvailable(port string) error {
conn, err := net.DialTimeout("tcp", "localhost:"+port, time.Second)
if err != nil {
return nil
}
_ = conn.Close()
return fmt.Errorf("port %s already in use", port)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/container/start.go` around lines 157 - 167, In checkPortAvailable,
when net.DialTimeout succeeds it indicates the port is in use, so always return
the "port %s already in use" error regardless of conn.Close() result; do not
return nil on a Close error. Update the function (checkPortAvailable) to defer
or call conn.Close() but ignore or log any Close error and then return
fmt.Errorf("port %s already in use", port) after a successful DialTimeout,
ensuring Dial success drives the error path.

Comment on lines +88 to +104
func TestStartCommandDoesNothingWhenAlreadyRunning(t *testing.T) {
requireDocker(t)
cleanup()
t.Cleanup(cleanup)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

startTestContainer(t, ctx)

cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=fake-token")
output, err := cmd.CombinedOutput()

require.NoError(t, err, "lstk start should succeed when container is already running: %s", output)
assert.Contains(t, string(output), "already running")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for env helper and AuthToken definition
fd -t f "env" test/integration/ | head -20

Repository: localstack/lstk

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Look for the env package structure
find test/integration -type f -name "*.go" | xargs grep -l "env\\.With\\|env\\.AuthToken" | head -10

Repository: localstack/lstk

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Search for AuthToken definition in the env helper
rg -n "AuthToken\s*=" test/integration/ --type go

Repository: localstack/lstk

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check if the start command uses Bubble Tea
rg -n "Bubble Tea|bubbletea" --type go | head -20

Repository: localstack/lstk

Length of output: 512


🏁 Script executed:

#!/bin/bash
# Check other tests in start_test.go to see env.With usage
head -100 test/integration/start_test.go | rg -n "env\\.With\\|os\\.Environ"

Repository: localstack/lstk

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Read the env helper file to see what AuthToken maps to
cat test/integration/env/env.go

Repository: localstack/lstk

Length of output: 1110


🏁 Script executed:

#!/bin/bash
# Check the start_test.go file to see env.With and os.Environ patterns
cat test/integration/start_test.go

Repository: localstack/lstk

Length of output: 4026


🏁 Script executed:

#!/bin/bash
# Search for where LOCALSTACK_AUTH_TOKEN or similar env vars are used
rg -n "LOCALSTACK_AUTH_TOKEN|LSTK_AUTH_TOKEN" --type go

Repository: localstack/lstk

Length of output: 937


🏁 Script executed:

#!/bin/bash
# Check if start command has Bubble Tea UI
rg -n "tea\\.|bubbletea" cmd/ internal/ --type go | grep -i start

Repository: localstack/lstk

Length of output: 41


Use env.With() for environment variable consistency and correct env var name.

This test (and the one at line 106) use append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=fake-token"), inconsistent with earlier tests in the file that use env.With(). More importantly, LOCALSTACK_AUTH_TOKEN is incorrect—that's the variable the CLI passes to the container. The CLI reads from LSTK_AUTH_TOKEN (via env.AuthToken). Use env.With(env.AuthToken, "fake-token") instead.

♻️ Suggested fix
-	cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=fake-token")
+	cmd.Env = env.With(env.AuthToken, "fake-token")
📝 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
func TestStartCommandDoesNothingWhenAlreadyRunning(t *testing.T) {
requireDocker(t)
cleanup()
t.Cleanup(cleanup)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
startTestContainer(t, ctx)
cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = append(os.Environ(), "LOCALSTACK_AUTH_TOKEN=fake-token")
output, err := cmd.CombinedOutput()
require.NoError(t, err, "lstk start should succeed when container is already running: %s", output)
assert.Contains(t, string(output), "already running")
}
func TestStartCommandDoesNothingWhenAlreadyRunning(t *testing.T) {
requireDocker(t)
cleanup()
t.Cleanup(cleanup)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
startTestContainer(t, ctx)
cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = env.With(env.AuthToken, "fake-token")
output, err := cmd.CombinedOutput()
require.NoError(t, err, "lstk start should succeed when container is already running: %s", output)
assert.Contains(t, string(output), "already running")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/start_test.go` around lines 88 - 104, The test
TestStartCommandDoesNothingWhenAlreadyRunning uses append(os.Environ(), ...) and
the wrong env var name; replace that with the project's env helper so the CLI
reads the correct token: use env.With(env.AuthToken, "fake-token") for the
cmd.Env setup (the same approach used in other tests), i.e. update the
exec.CommandContext invocation in TestStartCommandDoesNothingWhenAlreadyRunning
to set cmd.Env via env.With and reference env.AuthToken instead of
"LOCALSTACK_AUTH_TOKEN" so startTestContainer and the CLI see the correct
LSTK_AUTH_TOKEN value.

Copy link
Copy Markdown
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

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

LGTM!

@carole-lavillonniere carole-lavillonniere merged commit ceb2095 into main Feb 24, 2026
6 checks passed
@carole-lavillonniere carole-lavillonniere deleted the carole/drg-357 branch February 24, 2026 08:27
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.

2 participants