Skip to content

Better error for missing container runtime#46

Open
gtsiolis wants to merge 3 commits intomainfrom
george/des-114
Open

Better error for missing container runtime#46
gtsiolis wants to merge 3 commits intomainfrom
george/des-114

Conversation

@gtsiolis
Copy link
Member

Here's an attempt to resolve DES-114 and DRG-538. Early draft having fun with CLI UX, anything missing?

CLIv1 CLIv2 BEFORE CLIv2 AFTER
Screenshot 2026-02-20 at 21 43 34 Screenshot 2026-02-20 at 21 43 24 Screenshot 2026-02-20 at 21 42 30

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ 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 fe08870 and 9a75849.

📒 Files selected for processing (2)
  • internal/ui/error_output.go
  • internal/ui/styles/styles.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/ui/error_output.go

📝 Walkthrough

Walkthrough

Refactors runtime initialization to a centralized context-aware helper, adds container engine pre-flight checks with a RuntimeUnavailableError, improves container name-conflict error formatting, and introduces structured startup error output and new UI styles.

Changes

Cohort / File(s) Summary
CLI runtime usage
cmd/logs.go, cmd/root.go, cmd/start.go, cmd/stop.go
Replaced direct runtime.NewDockerRuntime() calls with newRuntime(cmd.Context()); replaced ad-hoc startup error exits with ui.ExitWithStartError() in root/start flows.
Runtime helper & engine checks
cmd/runtime.go, internal/runtime/engine_check.go, internal/runtime/docker.go
Added newRuntime(ctx) helper that creates runtime and runs CheckContainerEngine; introduced RuntimeUnavailableError, CheckContainerEngine(), and DockerRuntime.CheckConnection() to detect daemon connectivity issues.
Container conflict detection
internal/container/errors.go, internal/container/start.go, internal/container/errors_test.go
Added formatContainerNameConflict() and applied it in removal/start paths to surface friendly container name conflict errors; new unit tests validate behavior.
UI error output & styles
internal/ui/error_output.go, internal/ui/error_output_test.go, internal/ui/styles/styles.go
New ExitWithStartError(err) that renders structured startup errors (including runtime-unavailable details) and exits; added multiple error-specific Lipgloss styles and tests for detail summarization.
Tests & mocks
internal/runtime/engine_check_test.go, internal/runtime/mock_runtime_test.go, internal/runtime/runtime.go
Added engine-check unit tests with fakeRuntime, generated GoMock MockRuntime, and a go:generate directive to keep mocks in sync.

Sequence Diagram(s)

sequenceDiagram
    participant Cmd as CLI Command
    participant Helper as newRuntime()
    participant Runtime as DockerRuntime
    participant Daemon as Docker Daemon
    participant UI as UI Error Handler

    Cmd->>Helper: newRuntime(ctx)
    Helper->>Runtime: runtime.NewDockerRuntime()
    Runtime-->>Helper: rt or error
    alt creation error
        Helper-->>Cmd: error
        Cmd->>UI: ExitWithStartError(err)
        UI-->>Cmd: Exit(1)
    else created
        Helper->>Runtime: CheckConnection(ctx) via CheckContainerEngine
        Runtime->>Daemon: Ping()
        alt ping fails
            Daemon-->>Helper: connection error
            Helper-->>Cmd: RuntimeUnavailableError
            Cmd->>UI: ExitWithStartError(err)
            UI-->>Cmd: Exit(1)
        else ping ok
            Daemon-->>Helper: success
            Helper-->>Cmd: rt
            Cmd->>Cmd: proceed with command logic
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • carole-lavillonniere
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Better error for missing container runtime' directly summarizes the main purpose of the changeset, which introduces improved error handling and messaging for cases where a container runtime is unavailable.
Description check ✅ Passed The description is related to the changeset, explaining the intent to improve error messages for missing container runtime and referencing specific issues (DES-114 and DRG-538) with visual comparisons of error output improvements.

✏️ 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 george/des-114

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

@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 (3)
cmd/error_output.go (1)

12-21: Consider moving error styles to internal/ui/styles.

The error output styles are defined inline here. Based on coding guidelines, semantic styles are typically defined in internal/ui/styles/styles.go. However, if these styles are intentionally scoped to CLI-only error output (separate from TUI), this is acceptable.

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

In `@cmd/error_output.go` around lines 12 - 21, The error style variables
(errorTitleStyle, errorBodyStyle, errorNoteStyle, errorSummaryPrefix,
errorActionArrowStyle, errorActionTextStyle, errorLinkStyle, errorK8sStyle) are
declared inline in cmd/error_output.go and should be relocated to the shared
semantic styles package to follow project conventions; move their definitions
into internal/ui/styles (e.g., styles.go) as exported or package-level variables
and import/use them from cmd/error_output.go (or if these styles are
intentionally CLI-only, add a brief comment above the block explaining they are
CLI-scoped to justify keeping them here).
cmd/stop.go (1)

23-30: Pre-existing pattern: onProgress callback passed to domain layer.

The onProgress callback passed to container.Stop may conflict with the guideline to emit typed events through internal/output rather than passing UI callbacks through domain layers. This is pre-existing code, so it's not blocking for this PR, but could be addressed in a future refactor for consistency with the output event pattern used elsewhere.

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

In `@cmd/stop.go` around lines 23 - 30, The code passes a UI callback `onProgress`
into the domain call `container.Stop`, which contradicts the project pattern of
emitting typed events via the `internal/output` event system; refactor by
replacing the `onProgress` callback use with emission of appropriate
`internal/output` events (or an adapter that converts progress messages into
those events) before calling `container.Stop`, and update `container.Stop` (and
any related domain APIs) to accept no UI callbacks (or an event-emitter
interface) so progress is reported through the `internal/output` event types
instead of the `onProgress` function.
internal/runtime/engine_check.go (1)

62-65: Consider expanding the Docker daemon unavailability heuristics.

The current checks cover common error messages, but Docker can emit additional patterns depending on the environment (e.g., "connection refused", "no such host", "permission denied" for socket access). These would currently fall through to the generic error path on line 59, which is acceptable but produces a less user-friendly message.

This is fine to defer if the current coverage meets your needs—the fallback still provides a clear error with the endpoint context.

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

In `@internal/runtime/engine_check.go` around lines 62 - 65, Expand the Docker
daemon unavailability heuristics in isDockerDaemonUnavailable by checking
additional common error substrings (e.g., "connection refused", "no such host",
"permission denied", "docker.sock", "dial unix") and make the match
case-insensitive (use strings.ToLower(msg)) so these variants are recognized;
update the function isDockerDaemonUnavailable to include these extra contains
checks (or a small slice+loop of patterns) so more environment-specific daemon
errors return true and trigger the friendly endpoint-specific message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/error_output.go`:
- Around line 50-57: The fmt.Fprintln call in printActionLine currently ignores
its returned (n, error); change it to explicitly discard the results or handle
the error—e.g., replace the plain fmt.Fprintln(...) with an assignment like "_,
_ = fmt.Fprintln(...)" inside the printActionLine function so the returned
values are not unchecked (or log/return the error if preferred).
- Around line 39-48: The linter flags an unchecked error from fmt.Fprintln
inside printIndentedStyled; update printIndentedStyled to handle the returned
(int, error) by capturing and explicitly handling or ignoring the error to
satisfy the linter—e.g., assign the returned values and, if desired, log or
discard the error via a named blank identifier or a short conditional that
ignores it; ensure the change is applied to the fmt.Fprintln call(s) inside the
printIndentedStyled function so the error return is no longer unchecked.

---

Nitpick comments:
In `@cmd/error_output.go`:
- Around line 12-21: The error style variables (errorTitleStyle, errorBodyStyle,
errorNoteStyle, errorSummaryPrefix, errorActionArrowStyle, errorActionTextStyle,
errorLinkStyle, errorK8sStyle) are declared inline in cmd/error_output.go and
should be relocated to the shared semantic styles package to follow project
conventions; move their definitions into internal/ui/styles (e.g., styles.go) as
exported or package-level variables and import/use them from cmd/error_output.go
(or if these styles are intentionally CLI-only, add a brief comment above the
block explaining they are CLI-scoped to justify keeping them here).

In `@cmd/stop.go`:
- Around line 23-30: The code passes a UI callback `onProgress` into the domain
call `container.Stop`, which contradicts the project pattern of emitting typed
events via the `internal/output` event system; refactor by replacing the
`onProgress` callback use with emission of appropriate `internal/output` events
(or an adapter that converts progress messages into those events) before calling
`container.Stop`, and update `container.Stop` (and any related domain APIs) to
accept no UI callbacks (or an event-emitter interface) so progress is reported
through the `internal/output` event types instead of the `onProgress` function.

In `@internal/runtime/engine_check.go`:
- Around line 62-65: Expand the Docker daemon unavailability heuristics in
isDockerDaemonUnavailable by checking additional common error substrings (e.g.,
"connection refused", "no such host", "permission denied", "docker.sock", "dial
unix") and make the match case-insensitive (use strings.ToLower(msg)) so these
variants are recognized; update the function isDockerDaemonUnavailable to
include these extra contains checks (or a small slice+loop of patterns) so more
environment-specific daemon errors return true and trigger the friendly
endpoint-specific message.

Comment on lines 39 to 48
func printIndentedStyled(out *os.File, text, indent string, style lipgloss.Style) {
lines := strings.Split(text, "\n")
for i, line := range lines {
lineIndent := indent
if i > 0 {
lineIndent += " "
}
fmt.Fprintln(out, lineIndent+style.Render(line))
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unchecked error return from fmt.Fprintln.

The linter flags that the return value of fmt.Fprintln is not checked. While the impact is low (this is error output right before exit), addressing the lint warning keeps the codebase clean.

Proposed fix
 func printIndentedStyled(out *os.File, text, indent string, style lipgloss.Style) {
 	lines := strings.Split(text, "\n")
 	for i, line := range lines {
 		lineIndent := indent
 		if i > 0 {
 			lineIndent += "  "
 		}
-		fmt.Fprintln(out, lineIndent+style.Render(line))
+		_, _ = fmt.Fprintln(out, lineIndent+style.Render(line))
 	}
 }
📝 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 printIndentedStyled(out *os.File, text, indent string, style lipgloss.Style) {
lines := strings.Split(text, "\n")
for i, line := range lines {
lineIndent := indent
if i > 0 {
lineIndent += " "
}
fmt.Fprintln(out, lineIndent+style.Render(line))
}
}
func printIndentedStyled(out *os.File, text, indent string, style lipgloss.Style) {
lines := strings.Split(text, "\n")
for i, line := range lines {
lineIndent := indent
if i > 0 {
lineIndent += " "
}
_, _ = fmt.Fprintln(out, lineIndent+style.Render(line))
}
}
🧰 Tools
🪛 GitHub Check: Lint

[failure] 46-46:
Error return value of fmt.Fprintln is not checked (errcheck)

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

In `@cmd/error_output.go` around lines 39 - 48, The linter flags an unchecked
error from fmt.Fprintln inside printIndentedStyled; update printIndentedStyled
to handle the returned (int, error) by capturing and explicitly handling or
ignoring the error to satisfy the linter—e.g., assign the returned values and,
if desired, log or discard the error via a named blank identifier or a short
conditional that ignores it; ensure the change is applied to the fmt.Fprintln
call(s) inside the printIndentedStyled function so the error return is no longer
unchecked.

Comment on lines 50 to 57
func printActionLine(out *os.File, label, value string, labelStyle lipgloss.Style) {
fmt.Fprintln(
out,
errorActionArrowStyle.Render("⇒")+" "+
labelStyle.Render(label)+" "+
errorLinkStyle.Render(value),
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unchecked error return from fmt.Fprintln.

Same lint issue here—consider using _, _ = to explicitly discard the return values.

Proposed fix
 func printActionLine(out *os.File, label, value string, labelStyle lipgloss.Style) {
-	fmt.Fprintln(
+	_, _ = fmt.Fprintln(
 		out,
 		errorActionArrowStyle.Render("⇒")+" "+
 			labelStyle.Render(label)+" "+
 			errorLinkStyle.Render(value),
 	)
 }
🧰 Tools
🪛 GitHub Check: Lint

[failure] 51-51:
Error return value of fmt.Fprintln is not checked (errcheck)

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

In `@cmd/error_output.go` around lines 50 - 57, The fmt.Fprintln call in
printActionLine currently ignores its returned (n, error); change it to
explicitly discard the results or handle the error—e.g., replace the plain
fmt.Fprintln(...) with an assignment like "_, _ = fmt.Fprintln(...)" inside the
printActionLine function so the returned values are not unchecked (or log/return
the error if preferred).

@gtsiolis gtsiolis self-assigned this Feb 21, 2026
Copy link
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.

What's blocking is that we're defining too much logic in the cmd package. That one is just for surface level orchestration. Styling etc. should all happen in the UI package

Comment on lines 13 to 20
errorTitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#D04029"))
errorBodyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
errorNoteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
errorSummaryPrefix = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
errorActionArrowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
errorActionTextStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("250"))
errorLinkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Bold(true)
errorK8sStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#A8C73A")).Bold(true)
Copy link
Member

Choose a reason for hiding this comment

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

blocker: this is not reusing our existing styles

Copy link
Member

Choose a reason for hiding this comment

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

blocker: the cmd package shouldn't have any styling logic. that's what the internal/ui package is for

Comment on lines 15 to 24
func (f *fakeRuntime) PullImage(context.Context, string, chan<- PullProgress) error { return nil }
func (f *fakeRuntime) Start(context.Context, ContainerConfig) (string, error) { return "", nil }
func (f *fakeRuntime) Stop(context.Context, string) error { return nil }
func (f *fakeRuntime) Remove(context.Context, string) error { return nil }
func (f *fakeRuntime) IsRunning(context.Context, string) (bool, error) { return false, nil }
func (f *fakeRuntime) Logs(context.Context, string, int) (string, error) { return "", nil }
func (f *fakeRuntime) StreamLogs(context.Context, string, io.Writer) error { return nil }
func (f *fakeRuntime) GetImageVersion(context.Context, string) (string, error) { return "", nil }
func (f *fakeRuntime) CheckConnection(context.Context) error { return f.err }

Copy link
Member

Choose a reason for hiding this comment

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

nit: it would be better to create a generated mock for this like we're doing in other tests

@gtsiolis
Copy link
Member Author

Thanks for taking a look and sharing insights on how TUI works, @silv-io! Rebased and pushed an update. ⏳

@gtsiolis gtsiolis requested a review from silv-io February 24, 2026 12:19
Copy link

@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)
cmd/root.go (1)

26-35: rootCmd.Run duplicates startCmd.Run verbatim — extract a shared handler.

Both lstk and lstk start run the same startup flow. Any future change to this logic (e.g., adding a flag or changing error handling) must be applied twice. Extracting a shared function eliminates the risk of the two commands drifting.

♻️ Proposed refactor: extract shared handler

In cmd/root.go (or a new cmd/start_handler.go):

+func runStartCmd(cmd *cobra.Command, _ []string) {
+	rt, err := newRuntime(cmd.Context())
+	if err != nil {
+		ui.ExitWithStartError(err)
+	}
+	if err := runStart(cmd.Context(), rt); err != nil {
+		ui.ExitWithStartError(err)
+	}
+}
+
 var rootCmd = &cobra.Command{
 	...
 	PreRunE: initConfig,
-	Run: func(cmd *cobra.Command, args []string) {
-		rt, err := newRuntime(cmd.Context())
-		if err != nil {
-			ui.ExitWithStartError(err)
-		}
-		if err := runStart(cmd.Context(), rt); err != nil {
-			ui.ExitWithStartError(err)
-		}
-	},
+	Run: runStartCmd,
 }

In cmd/start.go:

 var startCmd = &cobra.Command{
 	...
 	PreRunE: initConfig,
-	Run: func(cmd *cobra.Command, args []string) {
-		rt, err := newRuntime(cmd.Context())
-		if err != nil {
-			ui.ExitWithStartError(err)
-		}
-		if err := runStart(cmd.Context(), rt); err != nil {
-			ui.ExitWithStartError(err)
-		}
-	},
+	Run: runStartCmd,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/root.go` around lines 26 - 35, rootCmd.Run and startCmd.Run contain
identical startup logic (calling newRuntime, handling errors with
ui.ExitWithStartError, then runStart), so extract that sequence into a single
shared function (e.g., startHandler(ctx context.Context) error or
startHandler(ctx context.Context) that internally calls newRuntime and runStart
and invokes ui.ExitWithStartError on failures) and have both rootCmd.Run and
startCmd.Run delegate to that function; update calls to use the shared
startHandler and keep references to newRuntime, runStart, and
ui.ExitWithStartError so behavior remains identical.
🤖 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/ui/error_output.go`:
- Around line 14-47: The fmt.Fprintln/Fprintf calls in ExitWithStartError,
printIndentedStyled and printActionLine ignore returned errors (errcheck
failure); update each call to capture the returned error and handle it (e.g. if
wErr := fmt.Fprintln(...); wErr != nil { log.Printf("failed writing error
output: %v", wErr) } ), and do the same inside the loop in printIndentedStyled
and for the multi-part print in printActionLine so every write error is checked
and logged (use log.Printf or similar non-panicking reporting).

In `@internal/ui/styles/styles.go`:
- Around line 31-50: Staticcheck warns that Style.Copy() is deprecated because
lipgloss.Style is a value type; remove the unnecessary .Copy() calls and use the
base styles directly when building the error styles. Replace expressions like
ErrorTitle = Title.Copy().Foreground(...) with ErrorTitle =
Title.Foreground(...), ErrorBody = Message.Foreground(...), ErrorActionText =
Version.Foreground(...), ErrorLink = Message.Bold(true) (or Message.Copy
removed), and ErrorK8s = NimboMid.Bold(true), keeping the same chained setters
and assignments for ErrorNote, ErrorSummaryPrefix, and ErrorActionArrow which
should remain as Message (no Copy).

---

Nitpick comments:
In `@cmd/root.go`:
- Around line 26-35: rootCmd.Run and startCmd.Run contain identical startup
logic (calling newRuntime, handling errors with ui.ExitWithStartError, then
runStart), so extract that sequence into a single shared function (e.g.,
startHandler(ctx context.Context) error or startHandler(ctx context.Context)
that internally calls newRuntime and runStart and invokes ui.ExitWithStartError
on failures) and have both rootCmd.Run and startCmd.Run delegate to that
function; update calls to use the shared startHandler and keep references to
newRuntime, runStart, and ui.ExitWithStartError so behavior remains identical.

ℹ️ 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 f993cf4 and fe08870.

📒 Files selected for processing (16)
  • cmd/logs.go
  • cmd/root.go
  • cmd/runtime.go
  • cmd/start.go
  • cmd/stop.go
  • internal/container/errors.go
  • internal/container/errors_test.go
  • internal/container/start.go
  • internal/runtime/docker.go
  • internal/runtime/engine_check.go
  • internal/runtime/engine_check_test.go
  • internal/runtime/mock_runtime_test.go
  • internal/runtime/runtime.go
  • internal/ui/error_output.go
  • internal/ui/error_output_test.go
  • internal/ui/styles/styles.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/runtime/docker.go
  • cmd/stop.go
  • internal/container/errors_test.go
  • internal/runtime/engine_check_test.go
  • cmd/logs.go

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