Repository navigation
feat(client)!: report why the client could not connect to the engine - #677
Merged
Merged
Conversation
docker-agent
reviewed
Oct 8, 2026
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
One medium-severity finding on the Windows errno classification path.
Benehiko
force-pushed
the
feat/client-engine-not-running
branch
3 times, most recently
from
October 8, 2026 07:27
f31eb68 to
f44b2d6
Compare
Benehiko
commented
Oct 8, 2026
| import "syscall" | ||
|
|
||
| // errConnRefused is WSAECONNREFUSED; Winsock never returns syscall.ECONNREFUSED. | ||
| const errConnRefused = syscall.Errno(10061) |
Member
Author
There was a problem hiding this comment.
Tested on Windows: a Windows 11 VM (build 26100, ARM64), running the test binaries cross-compiled with GOOS=windows GOARCH=arm64 go test -c.
TestConnectErrorFromSocket dials real AF_UNIX sockets:
- Stale socket (
SetUnlinkOnClose(false), then close the listener): Winsock returnsWSAECONNREFUSED(10061), so this constant matches and the error isReasonNotRunning.syscall.ECONNREFUSEDwould not have matched. - Missing socket: reported as
fs.ErrNotExistviasyscall.Errno.Is, alsoReasonNotRunning.
The full client, client/dockerhub and plugins/pass/commands suites pass on the VM.
Not covered here: a deny-all ACL on the socket for the current user did not stop the connection, so ReasonPermissionDenied does not show up on Windows in that setup.
docker-agent
reviewed
Oct 8, 2026
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Benehiko
added a commit
to Benehiko/docker-agent
that referenced
this pull request
Oct 8, 2026
The SDK wraps every failed dial in ErrSecretsEngineNotAvailable, permission denied and timeouts included, so an engine we may not connect to was logged at debug level as if Docker Desktop were not installed. Only a missing socket or one nothing listens on is routine now; anything else gets the warning. Tests build the SDK's real error shape instead of wrapping a string. docker/secrets-engine#677 adds ErrSecretsEngineUnreachable to do this in the SDK.
Benehiko
force-pushed
the
feat/client-engine-not-running
branch
2 times, most recently
from
October 8, 2026 08:53
2ae84e2 to
74d43ee
Compare
joe0BAB
reviewed
Oct 8, 2026
joe0BAB
reviewed
Oct 8, 2026
joe0BAB
reviewed
Oct 8, 2026
joe0BAB
reviewed
Oct 8, 2026
joe0BAB
reviewed
Oct 8, 2026
Every dial failure was wrapped in ErrSecretsEngineNotAvailable, so a caller could not tell an engine that is not running from one it may not connect to. Replace it with ConnectError, which carries a Reason (NotRunning, PermissionDenied, Timeout or Unknown), the socket path and the underlying dial error. Match any connection failure with errors.As, or one reason with errors.Is and ErrSecretsEngineNotRunning, ErrSecretsEnginePermissionDenied or ErrSecretsEngineTimeout. A cancelled context is returned unchanged rather than reported as an engine failure. pass: preflightPing reports an engine that accepts the connection but does not answer as a ReasonTimeout ConnectError, and passes other engine errors through. deadSocket now uses a short path; the t.TempDir() path exceeded the Unix socket path limit on macOS and failed with EINVAL. BREAKING CHANGE: ErrSecretsEngineNotAvailable is removed. Use errors.As with *ConnectError, or errors.Is with one of the new sentinels. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
preflightPing labelled any context.DeadlineExceeded as a ReasonTimeout ConnectError, including when the caller's own context expired before the ping timeout. Record errPingTimeout as the cause of the ping's own deadline and only wrap the error when context.Cause reports it; otherwise return the caller's error unchanged. ConnectErrors already classified by the client are returned as is. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Benehiko
force-pushed
the
feat/client-engine-not-running
branch
2 times, most recently
from
October 8, 2026 12:27
7cb0d3e to
04a51ec
Compare
Benehiko
enabled auto-merge
October 8, 2026 12:30
joe0BAB
reviewed
Oct 8, 2026
Address review feedback by dropping doc comments that restate the code in client/errors.go and plugins/pass/commands/client.go. Also fold the package-level connectError helper into the client.connectError method, its only caller. No behaviour change. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Benehiko
force-pushed
the
feat/client-engine-not-running
branch
from
October 8, 2026 12:33
04a51ec to
fadde1c
Compare
joe0BAB
approved these changes
Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every failure to dial the engine socket was wrapped in
ErrSecretsEngineNotAvailable. A missing socket, a stale one, permission denied and a dial timeout all looked the same. Clients that try the engine before falling back need to tell those apart: "not running" is routine and only worth a debug log, while "permission denied" or "timed out" means something is wrong. This came up in review of docker/docker-agent#4535, which currently has to dig the errno out of the wrapped error itself.What
Breaking:
ErrSecretsEngineNotAvailableis removed. It is replaced by one error type that says why the connection failed:Each reason except
ReasonUnknownhas a sentinel thaterrors.Ismatches:ErrSecretsEngineNotRunning,ErrSecretsEnginePermissionDenied,ErrSecretsEngineTimeout.fs.ErrNotExist), or nothing listens on it (ECONNREFUSED, orWSAECONNREFUSEDon Windows).fs.ErrPermission.context.DeadlineExceeded, or anet.ErrorwhoseTimeout()is true.context.Canceleddial is returned unchanged. The caller cancelled it, so it isn't reported as an engine failure.secrets engine is not running at /run/secrets-engine.sock: dial unix ...: connect: no such file or directory.The removal is deliberate. Keeping the old name with a narrower meaning would still compile but quietly change behaviour, whereas a build failure makes each caller pick the case it means.
Using it
Fall back on any connection failure (e.g. when the engine is optional):
Treat "not running" as routine and surface everything else:
Branch on the reason, and use the socket path in a message:
Migrating
errors.Is(err, client.ErrSecretsEngineNotAvailable)errors.As/errors.AsType[*client.ConnectError](err)for any connection failuresyscall.ENOENT/ECONNREFUSEDout of the errorerrors.Is(err, client.ErrSecretsEngineNotRunning)errors.AsTypeneeds Go 1.26, which this module already requires.errors.Aswith a*client.ConnectErrortarget works too.pass
preflightPingno longer wraps every error as "not available":ConnectErrorwithReasonTimeout.context.WithTimeoutCause, and the error is wrapped only whencontext.Causereports that timeout. If the caller's context expires first, itscontext.DeadlineExceededis returned unchanged rather than reported as a hung engine.ConnectErrors already classified by the client, and real errors returned by the engine, pass through unchanged.Fixed a test helper:
deadSocket()built its path fromt.TempDir(), which on macOS is longer than the Unix socket path limit (~104 bytes). The tests were failing withEINVAL, not "no such file", and only passed because every failure counted as "not available". It now usestesthelper.RandomShortSocketName().Windows
Tested on a Windows 11 VM (build 26100, ARM64). Test binaries were cross-compiled with
GOOS=windows GOARCH=arm64 go test -cand run on the VM:clientclient/dockerhubplugins/pass/commandsTestConnectErrorFromSocketdials real AF_UNIX sockets. A missing socket and a stale socket (nothing listening) are both classifiedReasonNotRunning. That confirms Windows reports a missing socket asfs.ErrNotExistand a refused connection asWSAECONNREFUSED(10061). See the inline comment onerrno_windows.go.icacls <sock> /deny %USERNAME%:(F)) for the current user still connected successfully. So on Windows, at least in that setup,ReasonPermissionDenieddoes not occur; it is effectively Unix-only. Other users or ACL setups were not tested. The end-to-end permission test is skipped on Windows.Windows client tests still aren't in CI (only
keychain.ymlruns on Windows).Tests
TestConnectError: a table test of the real error shape (*net.OpError→*os.SyscallError→ errno). Each case checks the reason, the socket path, that only the matching sentinel matches, and that the dial error is still inspectable. Also covers cancelled contexts and the message format.TestConnectErrorFromSocket: end to end throughGetSecretsagainst a missing socket, a stale socket, and achmod 000socket (the last is skipped on Windows and as root).TestPreflightPing: not running, a non-connection engine error, a hung engine (→ErrSecretsEngineTimeout), and a caller deadline that fires before the ping timeout (→context.DeadlineExceeded, notErrSecretsEngineTimeout).go vet(darwin, windows) andgolangci-lintare clean forclientandplugins/pass.credentialhelper,pluginandstorebuild.