Skip to content

Conversation

@skevetter
Copy link
Owner

@skevetter skevetter commented Feb 6, 2026

  • refactor(sshtunnel): ExecuteCommand to use options struct
  • fix: pipe closures
  • fix: minor touchup comments

Summary by CodeRabbit

  • Chores

    • Added a lint rule to enforce a limit on function result counts.
  • Refactor

    • Command execution and SSH tunnel handling moved to an options-based API for clearer configuration.
    • Tunnel and agent workflows modularized with improved concurrency, retries, error handling, and logging for more reliable SSH operations.
    • Optional reuse of existing SSH sessions to reduce session churn.

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.
📝 Walkthrough

Walkthrough

Replace positional sshtunnel.ExecuteCommand parameters with a new ExecuteCommandOptions struct; refactor sshtunnel internals (pipes, goroutines, backoff, agent forwarding, log streaming); update call sites in cmd/up.go and pkg/devcontainer/setup.go; add revive lint rule function-result-limit: [3]; allow using a pre-created SSH session in pkg/ssh/helper.go.

Changes

Cohort / File(s) Summary
Configuration & Linting
./.golangci.yaml
Added revive lint rule function-result-limit with argument [3].
SSHTunnel public API
pkg/devcontainer/sshtunnel/sshtunnel.go
Added ExecuteCommandOptions type and changed ExecuteCommand signature to accept the options struct (migration from multiple positional params).
SSHTunnel implementation
pkg/devcontainer/sshtunnel/sshtunnel.go
Large refactor: introduced tunnel and pipePair abstractions, errgroup-based concurrency, SSH server readiness/backoff, agent forwarding and key handling, runCommandInSSHTunnel, TunnelLogStreamer, and enhanced error propagation/cleanup.
Callsite updates
cmd/up.go, pkg/devcontainer/setup.go
Replaced positional ExecuteCommand(...) calls with ExecuteCommand(ExecuteCommandOptions{...}); moved prior tunnel-server invocation into TunnelServerFunc closures and pass AgentInject via options.
SSH helper API
pkg/ssh/helper.go
Added RunOptions.Session *ssh.Session to allow using a provided SSH session; conditional session lifecycle handling (create vs reuse).

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Caller
participant SSHTunnel as sshtunnel.ExecuteCommand
participant TunnelServer as TunnelServerFunc
participant SSHClient as SSH Client
participant RemoteCmd as Remote Command
Caller->>SSHTunnel: ExecuteCommand(ExecuteCommandOptions)
SSHTunnel->>SSHTunnel: create pipes, start goroutines (errgroup)
SSHTunnel->>TunnelServer: invoke TunnelServerFunc(stdin, stdout)
TunnelServer-->>SSHTunnel: server ready / pipes connected
SSHTunnel->>SSHClient: establish tunnel (with backoff)
SSHTunnel->>SSHClient: setupSSHAgentForwarding / inject keys
SSHTunnel->>RemoteCmd: runCommandInSSHTunnel
RemoteCmd-->>SSHClient: command output / exit status
SSHClient-->>SSHTunnel: session result
SSHTunnel->>SSHTunnel: aggregate logs/errors, wait for goroutines
SSHTunnel->>Caller: return consolidated Result / error

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: refactoring ExecuteCommand to use an options struct pattern.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-ssh-tunnel

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.

@skevetter skevetter marked this pull request as ready for review February 6, 2026 18:28
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: 3

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 89-92: waitForTunnelCompletion may return an error from
TunnelServerFunc while goroutines are still blocked, causing wg.Wait to deadlock
because tc.cleanup (which cancels context/closes pipes) is deferred until
ExecuteCommand returns; modify ExecuteCommand to cancel the tunnel context
and/or call tc.cleanup immediately after receiving an error from
waitForTunnelCompletion (before calling wg.Wait) and ensure you still drain
errChan if needed so the goroutines can return and wg.Wait can complete.
Specifically, update the error path around the call to waitForTunnelCompletion
(reference symbols: waitForTunnelCompletion, TunnelServerFunc, wg.Wait, errChan,
tc.cleanup, ExecuteCommand) to invoke tc.cancel() or tc.cleanup() (and drain
errChan) prior to waiting on the WaitGroup.
- Around line 215-229: The goroutine uses p.opts.Ctx (the external context) when
calling AgentInject which ignores the internal cancellable context and can block
after siblings call cancel(); update executeSSHServerHelper to pass the
cancellable context instead. Add a cancellable context field to
sshServerHelperParams (e.g., cancelCtx or ctx), ensure the creator of
sshServerHelperParams sets it to the local cancelCtx produced alongside
cancel(), and call p.opts.AgentInject with that cancellable context (replace
p.opts.Ctx) so AgentInject sees internal cancellations; keep all other logic
(defer p.cancel(), error handling) unchanged.
- Around line 276-295: getSSHBackoff builds an exponential wait.Backoff using
ExecuteCommandOptions.SSHRetryInterval and SSHTimeout but omits a Cap, allowing
per-step sleeps to grow past the intended timeout and making total retry time
far larger; fix by setting wait.Backoff.Cap to a reasonable maximum (e.g., 5s or
configurable) so exponential growth is bounded, and adjust Steps calculation if
desired to reflect capped durations; update getSSHBackoff to populate the Cap
field on the returned wait.Backoff and document or make the cap configurable via
ExecuteCommandOptions if needed.
🧹 Nitpick comments (1)
pkg/devcontainer/sshtunnel/sshtunnel.go (1)

153-192: pipePair.Close doesn't nil out fields after closing, allowing double-close on the same fd.

cleanup() is deferred in ExecuteCommand, but the same pipePair fds could also be closed by goroutines or error paths. Closing an *os.File twice is benign (returns an error, which is discarded) unless the fd number is reused by the OS between the two closes, which would silently close an unrelated fd. Setting fields to nil after close guards against this.

Optional fix — nil-guard after close
 func (p *pipePair) Close() {
 	if p.stdoutReader != nil {
 		_ = p.stdoutReader.Close()
+		p.stdoutReader = nil
 	}
 	if p.stdoutWriter != nil {
 		_ = p.stdoutWriter.Close()
+		p.stdoutWriter = nil
 	}
 	if p.stdinReader != nil {
 		_ = p.stdinReader.Close()
+		p.stdinReader = nil
 	}
 	if p.stdinWriter != nil {
 		_ = p.stdinWriter.Close()
+		p.stdinWriter = nil
 	}
 }

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

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 246-275: The session returned from waitForSSHSession in
runSSHTunnel is configured for agent forwarding via setupSSHAgentForwarding but
then discarded because devssh.Run (used inside runCommandInSSHTunnel) creates a
new session; fix by ensuring the forwarding is requested on the actual session
used to run the command: modify runCommandInSSHTunnel to accept the prepared
sess (or alternatively change runCommandInSSHTunnel to call
setupSSHAgentForwarding on the session created inside it) and then call
devssh.Run with that same sess so the RequestAgentForwarding applied in
setupSSHAgentForwarding takes effect; reference functions: runSSHTunnel,
waitForSSHSession, setupSSHAgentForwarding, runCommandInSSHTunnel, and
devssh.Run.
🧹 Nitpick comments (2)
pkg/devcontainer/sshtunnel/sshtunnel.go (2)

162-179: pipePair.Close is not safe for concurrent calls.

Setting fields to nil after close makes repeat calls safe within a single goroutine, but there's no synchronization. Currently this is fine because cleanup() is only called once after wg.Wait(), but it's fragile if the calling pattern changes.

Optional: use sync.Once to make Close idempotent
 type pipePair struct {
+	once         sync.Once
 	stdoutReader *os.File
 	stdoutWriter *os.File
 	stdinReader  *os.File
 	stdinWriter  *os.File
 }
 
 func (p *pipePair) Close() {
-	if p.stdoutReader != nil {
-		_ = p.stdoutReader.Close()
-		p.stdoutReader = nil
-	}
-	if p.stdoutWriter != nil {
-		_ = p.stdoutWriter.Close()
-		p.stdoutWriter = nil
-	}
-	if p.stdinReader != nil {
-		_ = p.stdinReader.Close()
-		p.stdinReader = nil
-	}
-	if p.stdinWriter != nil {
-		_ = p.stdinWriter.Close()
-		p.stdinWriter = nil
-	}
+	p.once.Do(func() {
+		_ = p.stdoutReader.Close()
+		_ = p.stdoutWriter.Close()
+		_ = p.stdinReader.Close()
+		_ = p.stdinWriter.Close()
+	})
 }

337-358: Agent forwarding failure is treated as fatal — is this intentional?

setupSSHAgentForwarding logs a warning (Line 355) and returns the error (Line 357), which causes runSSHTunnel to send to errChan and abort (Lines 271-272). In many SSH workflows, agent forwarding is best-effort — it's useful when available but not critical for the tunnel itself.

If this is intentional (e.g., required for downstream commands), a brief comment documenting the rationale would help. Otherwise, consider logging the warning and continuing:

Optional: make agent forwarding best-effort
 	if err != nil {
 		tc.opts.Log.Warnf("SSH agent forwarding failed: %v", err)
 	}
-	return err
+	return nil
 }

Signed-off-by: Samuel K <skevetter@pm.me>
Signed-off-by: Samuel K <skevetter@pm.me>
Signed-off-by: Samuel K <skevetter@pm.me>
@skevetter skevetter force-pushed the refactor-ssh-tunnel branch from dc9de0c to 110b248 Compare February 8, 2026 19:36
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: 1

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 246-274: The SSH agent forwarding is being requested on the
session returned by waitForSSHSession and setupSSHAgentForwarding, but
devssh.Run (used inside runCommandInSSHTunnel) creates its own session so the
forwarding is never applied; fix by ensuring the session that has
RequestAgentForwarding is the one used for command execution—either change
runCommandInSSHTunnel/devssh.Run to accept and use the pre-configured sess
returned by waitForSSHSession (and remove the unused deferred sess close there),
or modify devssh.Run to call RequestAgentForwarding on its internal session when
tc indicates agent forwarding is desired; update runSSHTunnel to pass the
pre-configured session (sess) into runCommandInSSHTunnel, or implement the
RequestAgentForwarding call inside devssh.Run accordingly.
🧹 Nitpick comments (3)
pkg/devcontainer/sshtunnel/sshtunnel.go (3)

155-198: pipePair.Close is not safe for concurrent use.

The nil-check-then-close-then-nil-set pattern on lines 163–178 is not atomic. If Close() were called from two goroutines simultaneously, both could pass the nil check before either sets the pointer to nil, causing a double-close on the underlying *os.File.

In the current code, cleanup() is only called once (deferred in ExecuteCommand), so this isn't actively exploitable. But given that Close() is exported on a public-ish type and explicitly tries to be idempotent, consider adding a sync.Once to make it truly safe.


337-358: Agent forwarding failure is treated as fatal — is this intentional?

If ForwardToRemote or RequestAgentForwarding fails, the error propagates back to runSSHTunnel (line 270–272), which sends it to errChan and returns — meaning the tunnel command never runs. Agent forwarding is typically a nice-to-have; killing the entire operation for it may be too aggressive, especially when the user has no SSH agent running.

Currently, the function logs a warning (line 355) and returns the error. Consider returning nil after the warning to allow the command to proceed without agent forwarding.

Proposed fix — downgrade to warning-only
 	if err != nil {
 		tc.opts.Log.Warnf("SSH agent forwarding failed: %v", err)
 	}
-	return err
+	return nil

304-335: Context-canceled errors are sent to errChan AND returned — caller must not double-send.

Lines 325–326 send the error to errChan and return it. The caller runSSHTunnel (line 262–263) checks err != nil and returns without sending to errChan again. This is correct, but the dual responsibility (callee sends to channel, caller checks return value) is a subtle contract.

Consider documenting the invariant that waitForSSHSession sends its own error to errChan on failure, so future maintainers of runSSHTunnel don't accidentally double-send.

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

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 334-355: In setupSSHAgentForwarding, always call
devsshagent.RequestAgentForwarding(sess) after attempting
devsshagent.ForwardToRemote(sshClient, identityAgent) instead of only when
ForwardToRemote fails; keep the existing Warnf logging for ForwardToRemote
errors and also log any error returned by RequestAgentForwarding, but do not
gate the call on the first call's success—this matches the pattern in
cmd/machine/ssh.go and ensures both the handler registration (ForwardToRemote)
and the session request (RequestAgentForwarding) are performed for tunnelContext
with ssh.Client and ssh.Session.
🧹 Nitpick comments (4)
pkg/ssh/helper.go (1)

179-193: Pre-existing goroutine leak in setupContextCancellation.

The exit channel is created locally and only closed by the goroutine's own defer. The case <-exit: branch in the select can never fire because nothing external signals it. If the context is never cancelled (e.g., command completes successfully before cancellation), this goroutine leaks until the process exits or ctx is eventually cancelled by a parent.

The typical fix is to return exit (or a done channel) and close it after sess.Run() returns:

Proposed fix
-func setupContextCancellation(ctx context.Context, sess *ssh.Session) error {
+func setupContextCancellation(ctx context.Context, sess *ssh.Session) (func(), error) {
 	if err := ctx.Err(); err != nil {
-		return fmt.Errorf("context already cancelled: %w", err)
+		return nil, fmt.Errorf("context already cancelled: %w", err)
 	}
 	exit := make(chan struct{})
 	go func() {
 		defer close(exit)
 		select {
 		case <-ctx.Done():
 			_ = sess.Signal(ssh.SIGINT)
 		case <-exit:
 		}
 	}()
-	return nil
+	return func() { close(exit) }, nil
 }

Then in Run:

-	if err := setupContextCancellation(opts.Context, sess); err != nil {
+	cleanup, err := setupContextCancellation(opts.Context, sess)
+	if err != nil {
 		return err
 	}
+	defer cleanup()

Note: close(exit) would be called twice (once by the caller, once by the goroutine's defer). Use sync.Once or restructure to avoid the double-close panic.

This is pre-existing, not introduced by this PR, so it can be deferred.

pkg/devcontainer/sshtunnel/sshtunnel.go (3)

129-153: Goroutine errors are silently dropped when TunnelServerFunc fails.

If TunnelServerFunc returns an error at line 131, waitForTunnelCompletion returns immediately without draining errChan. Goroutine errors are lost. While this doesn't cause a deadlock (errChan has capacity 2 and tc.cancel() at line 91 unblocks goroutines), it may complicate debugging since the root cause could be a goroutine failure that triggered the TunnelServerFunc failure.

Consider draining errChan on the early-return path or logging whatever errors are available:

Proposed improvement
 	result, err := tc.opts.TunnelServerFunc(tc.cancelCtx, tc.grpcPipes.stdinWriter, tc.grpcPipes.stdoutReader)
 	if err != nil {
+		tc.opts.Log.Debugf("tunnel server failed, draining goroutine errors")
 		return nil, fmt.Errorf("start tunnel server: %w", err)
 	}

Or more thoroughly, drain after cancel:

// In ExecuteCommand, after tc.cancel() and wg.Wait():
close(tc.errChan) // safe after wg.Wait()
for e := range tc.errChan {
    if e != nil {
        opts.Log.Debugf("goroutine error (after tunnel server failure): %v", e)
    }
}

155-179: pipePair.Close() is not concurrency-safe.

Close() reads and writes fields without synchronization. This is fine in the current code because cleanup() is only called after wg.Wait() ensures all goroutines have exited. However, since tc.cancel() is called before wg.Wait(), goroutines that are shutting down could theoretically race with a future refactor that calls Close() earlier. Consider adding a brief comment noting the thread-safety assumption, or adding a sync.Once guard.


279-299: Steps calculation is vastly over-provisioned but context timeout mitigates it.

With defaults (duration=500ms, timeout=60s), steps = max(10, 240) = 240. With exponential growth capped at 5s, the worst-case theoretical total exceeds 20 minutes. In practice, the context.WithTimeout at line 306 bounds the actual retry window to 60s. The excess steps are harmless but could confuse future readers who expect steps to approximate the timeout.

Consider computing steps based on the capped duration to better reflect the intended window:

// After cap is reached, each step ≈ cap duration.
// Approximate: initial steps + (remaining time / cap)
steps := max(10, int(timeout / (5 * time.Second)) + 10)

This is minor since the context provides the hard limit.

@skevetter skevetter force-pushed the refactor-ssh-tunnel branch from c96ae48 to 5287377 Compare February 8, 2026 21:31
@github-actions
Copy link

github-actions bot commented Feb 8, 2026

⚠️ This PR contains unsigned commits. To get your PR merged, please sign those commits (git rebase --exec 'git commit -S --amend --no-edit -n' @{upstream}) and force push them to this branch (git push --force-with-lease).

If you're new to commit signing, there are different ways to set it up:

Sign commits with gpg

Follow the steps below to set up commit signing with gpg:

  1. Generate a GPG key
  2. Add the GPG key to your GitHub account
  3. Configure git to use your GPG key for commit signing
Sign commits with ssh-agent

Follow the steps below to set up commit signing with ssh-agent:

  1. Generate an SSH key and add it to ssh-agent
  2. Add the SSH key to your GitHub account
  3. Configure git to use your SSH key for commit signing
Sign commits with 1Password

You can also sign commits using 1Password, which lets you sign commits with biometrics without the signing key leaving the local 1Password process.

Learn how to use 1Password to sign your commits.

Watch the demo

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: 3

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 322-344: The setupSSHAgentForwarding function treats
devsshagent.ForwardToRemote errors as fatal and returns the error, which aborts
the tunnel; change this so ForwardToRemote failures are non-fatal: if
devsshagent.ForwardToRemote(sshClient, identityAgent) returns an error, call
tc.opts.Log.Warnf (include the error and socket) and skip agent-forward setup
(do not call devsshagent.RequestAgentForwarding), then continue and return nil
so the tunnel proceeds without agent forwarding; keep RequestAgentForwarding as
an error only when ForwardToRemote succeeded.
- Around line 252-260: After waitForSSHSession returns a sess, ensure the
session is always closed on all paths: either add a defer sess.Close()
immediately after waitForSSHSession(...) to guarantee cleanup and then remove
the duplicate close inside runCommandInSSHTunnel, or explicitly call
sess.Close() in the error path when setupSSHAgentForwarding(...) fails before
returning; reference the functions waitForSSHSession, setupSSHAgentForwarding,
runCommandInSSHTunnel and the sess.Close() call to locate and update the code
accordingly.
- Around line 160-171: The Close method's helper uses a value parameter so f =
nil is a no-op and the pipePair fields (stdoutReader, stdoutWriter, stdinReader,
stdinWriter) remain non-nil, making Close non-idempotent; change Close to close
each field only if non-nil and then set the struct field to nil directly (e.g.,
check p.stdoutReader != nil, call Close(), then assign p.stdoutReader = nil) for
all four fields so repeated Close() calls are safe and avoid double-closing file
descriptors.
🧹 Nitpick comments (2)
pkg/devcontainer/sshtunnel/sshtunnel.go (2)

355-363: devssh.Run also receives the parent context instead of cancelCtx.

Line 357 passes tc.opts.Ctx. If tc.cancel() fires (e.g., sibling failure), the SSH session won't receive SIGINT via setupContextCancellation because it watches the wrong context. It will only terminate when pipes are closed, which may delay cleanup.

For consistency with the tunnel's internal cancellation, use tc.cancelCtx.

Proposed fix
 	if err := devssh.Run(devssh.RunOptions{
-		Context: tc.opts.Ctx,
+		Context: tc.cancelCtx,
 		Client:  sshClient,
 		Command: tc.opts.Command,

270-290: Steps calculation still assumes constant-duration retries, leading to a ~20-minute effective timeout.

With duration=500ms, timeout=60s, Factor=1.5, Cap=5s: steps = max(10, 240) = 240. Most steps hit the 5s cap, so total retry time ≈ 240 × 5s = 1200s (20 minutes), far exceeding the intended 60s timeout. The cancelCtx is created with WithCancel (not WithTimeout), so there's no hard deadline.

Consider either adding a context.WithTimeout around the backoff call or capping steps to max(10, int(timeout/cap)):

Option: fix steps calculation for capped backoff
-	steps := max(10, int(timeout/duration*2))
+	steps := max(10, int(timeout/(5*time.Second))+1) // align with Cap

Or wrap with a timeout:

timeoutCtx, timeoutCancel := context.WithTimeout(tc.cancelCtx, getSSHTimeout(tc.opts))
defer timeoutCancel()
// use timeoutCtx in ExponentialBackoffWithContext

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

🤖 Fix all issues with AI agents
In `@pkg/devcontainer/sshtunnel/sshtunnel.go`:
- Around line 142-164: If TunnelServerFunc returns an error in ExecuteCommand,
we must still wait for the errgroup goroutines to exit to avoid leaking
resources and races on t.sshClient/t.sshSession. After calling
opts.TunnelServerFunc(...) and seeing a non-nil err, call g.Wait() (using gctx)
before returning the error so runSSHServerHelper and runSSHTunnel have exited;
keep the existing deferred t.Close() to cancel the context and close pipes, but
ensure g.Wait() is awaited even on TunnelServerFunc failure. Reference symbols:
ExecuteCommand, TunnelServerFunc, g (errgroup), gctx, runSSHServerHelper,
runSSHTunnel, and t.Close.
- Around line 261-288: In waitForSSHSession, the Backoff config (Duration:500ms,
Factor:1.5, Cap:5s, Steps:20) triggers k8s wait behavior that zeroes Steps once
the increment hits Cap, causing the loop to stop after ~6 attempts; fix by
either removing Cap so the backoff can grow through all 20 Steps or by
recalculating Steps to reflect the capped growth (e.g., set Steps to the number
of retries you actually want given the 5s Cap), and update the backoff variable
used by ExponentialBackoffWithContext accordingly (adjust the backoff literal in
waitForSSHSession).
🧹 Nitpick comments (2)
pkg/ssh/helper.go (1)

124-134: validate() requires Client even when a pre-created Session is supplied.

When opts.Session != nil, opts.Client is never used inside Run (it's only used at line 150 for NewSession). A caller that already has a session would still need to pass a Client to satisfy validation. This is not a bug since current callers provide both, but it tightens the API unnecessarily.

Proposed fix
 func (opts *RunOptions) validate() error {
 	if opts.Context == nil {
 		return fmt.Errorf("context is required")
 	}
-	if opts.Client == nil {
+	if opts.Client == nil && opts.Session == nil {
 		return fmt.Errorf("SSH client is required")
 	}
 	if opts.Command == "" {
 		return fmt.Errorf("command is required")
 	}
 	return nil
 }

Also applies to: 142-158

pkg/devcontainer/sshtunnel/sshtunnel.go (1)

419-428: extractLogLevel won't match "warning" — only "warn" is in the map.

Some log frameworks emit "warning" instead of "warn". If that applies to the remote output, those lines will silently fall back to debug level. Purely cosmetic since the fallback is safe.

Optional: add "warning" alias
 var logLevelMap = map[string]logrus.Level{
 	"debug": logrus.DebugLevel,
 	"info":  logrus.InfoLevel,
 	"warn":  logrus.WarnLevel,
+	"warning": logrus.WarnLevel,
 	"error": logrus.ErrorLevel,
 	"fatal": logrus.FatalLevel,
 }

@skevetter skevetter force-pushed the refactor-ssh-tunnel branch from ed8a62e to 4e59504 Compare February 9, 2026 03:46
Signed-off-by: Samuel K <skevetter@pm.me>
Signed-off-by: Samuel K <skevetter@pm.me>
Signed-off-by: Samuel K <skevetter@pm.me>
Signed-off-by: Samuel K <skevetter@pm.me>
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