Skip to content

feat(pass): hint how to fix a failed connection to the secrets engine - #678

Merged
Benehiko merged 5 commits into
mainfrom
feat/pass-engine-hints
Oct 8, 2026
Merged

Benehiko merged 5 commits into
mainfrom
feat/pass-engine-hints

Conversation

@Benehiko

@Benehiko Benehiko commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Why

#677 gives the client a ConnectError that says why it could not reach the engine. Until now, pass printed a bare dial error, which tells a user nothing about what to do next.

What

  • SDK: a new ConnectError.Hint() method in the client returns a user friendly message to help recover from the error. Every SDK consumer can show the same wording. Error() is unchanged, so callers choose whether to print the hint.
  • pass: get --reveal, rm and run print that hint below the error.

The hint depends on ConnectError.Reason and on which socket failed. The secrets engine can run inside Docker Desktop or standalone, so the not-running and timeout hints are worded for the engine that listens on that socket:

  • Docker Desktop: the socket is api.DesktopSocketPath().
  • Standalone: the socket is api.StandaloneSocketPath().
  • Custom: any other socket, such as one set with WithSocketPath.
Reason Docker Desktop socket Standalone socket Custom socket
ReasonNotRunning Start Docker Desktop and retry: the secrets engine runs as part of it. Start the standalone secrets engine and retry: nothing is listening on "<socket path>". Start the secrets engine and retry: nothing is listening on "<socket path>".
ReasonTimeout The secrets engine did not respond. Restart Docker Desktop and retry. The standalone secrets engine on "<socket path>" did not respond. Restart it and retry. The secrets engine on "<socket path>" did not respond. Restart it and retry.
ReasonPermissionDenied Your user lacks permission to connect to "<socket path>". Grant it read and write access to the socket and retry. same same
ReasonUnknown Hint() returns ""; pass leaves the error unchanged same same

The socket path is quoted, so control characters in it cannot corrupt the output. If the error carries no socket path, "<socket path>" reads "the secrets engine socket".

Example, with the default (Docker Desktop) socket:

$ docker pass get --reveal foo
Error: preflight ping: secrets engine is not running at /…/docker-secrets-engine/engine.sock: unavailable: dial unix /…/docker-secrets-engine/engine.sock: connect: no such file or directory

Start Docker Desktop and retry: the secrets engine runs as part of it.
  • client: Hint() compares the cleaned socket path with api.DesktopSocketPath() and api.StandaloneSocketPath().
  • pass: a wrapEngineErrors / withEngineHint pair that mirrors the existing wrapKeychainErrors / withKeychainHint. It finds the ConnectError in the chain and appends its hint in the same \n\n<hint> format, and it wraps both PreRunE and RunE.
  • Error chain: the original error stays wrapped, so errors.Is(err, client.ErrSecretsEngineNotRunning) still matches.
  • rm: hints still appear when the connection error is joined with per-secret "not found" errors.

Notes

  • Release order: plugins/pass builds against the local client through a replace directive, but its go.mod requires client v0.1.2. Building pass outside this repo needs a client release that includes Hint(), followed by a bump in pass.
  • Preflight ping timeout: when pass's own 3-second preflight ping times out, the ConnectError it builds has no socket path, so the hint uses the custom-socket wording with "the secrets engine socket". This behaviour predates this PR.

Tests

  • client.TestConnectErrorHint covers the wording:
    • not running and timeout on the Docker Desktop, standalone and custom sockets
    • not running without a socket path
    • permission denied on a custom socket, on the Docker Desktop socket and without a path
    • quoting of a path with control characters
    • no hint for ReasonUnknown
  • commands.TestWithEngineHint covers how pass applies it:
    • a wrapped connection error
    • a connection error inside an errors.Join
    • the pass-through cases (nil, ReasonUnknown, ErrAccessDenied)
  • The get --reveal and rm tests against a missing socket now check that the custom-socket hint, with that socket's quoted path, is printed.
  • go vet (darwin, linux, windows) and golangci-lint are clean, and the client and plugins/pass tests pass on macOS. I have not run this PR on Windows; feat(client)!: report why the client could not connect to the engine #677 covers the Windows classification.

Closes docker/secrets-engine-private#729.

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟢 APPROVE

Lower-confidence findings (not posted inline)

The following low-severity observations were noted by the drafter. They did not reach the threshold for inline comments (all are low severity; verification was skipped per policy).

  • [low] plugins/pass/commands/client.go:37 — wrapEngineErrors wraps only RunE, unlike wrapKeychainErrors which also wraps PreRunE (no current impact — none of the callers use PreRunE today, but a future command with PreRunE would silently miss the engine hint)
  • [low / security] plugins/pass/commands/client.go:61 — unsanitised ce.SocketPath interpolated directly into the user-visible hint string; a path with embedded newlines or control characters could corrupt the output (consider fmt.Sprintf("%q", socket))
  • [low] plugins/pass/commands/client.go:37 — wrapEngineErrors silently no-ops when cmd.RunE is nil (command uses Run instead); no current callers are affected, but a Run-only command would receive no hint

@Benehiko
Benehiko force-pushed the feat/client-engine-not-running branch from 2ae84e2 to 74d43ee Compare October 8, 2026 08:53
@Benehiko
Benehiko force-pushed the feat/pass-engine-hints branch from d5a2b8a to b32c24f Compare October 8, 2026 09:12
@Benehiko
Benehiko force-pushed the feat/client-engine-not-running branch 3 times, most recently from 04a51ec to fadde1c Compare October 8, 2026 12:33
@Benehiko
Benehiko force-pushed the feat/pass-engine-hints branch from b32c24f to 0b63bb3 Compare October 8, 2026 12:40
@Benehiko
Benehiko changed the base branch from feat/client-engine-not-running to main October 8, 2026 12:40
Comment thread plugins/pass/commands/client.go Outdated
Comment thread plugins/pass/commands/client.go Outdated
Comment thread plugins/pass/commands/client.go Outdated
get --reveal, rm and run now append a hint to a client.ConnectError, based on its reason: start the engine when it is not running, restart it when it does not respond, and check access to the socket when permission is denied. The not-running and timeout hints name Docker Desktop only when the socket is Docker Desktop's; for any other socket, such as a standalone engine, they name the socket instead. Unknown reasons and other errors pass through unchanged, as with the keychain hints.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
@Benehiko
Benehiko force-pushed the feat/pass-engine-hints branch from 0b63bb3 to 4d604e3 Compare October 8, 2026 12:55
Tell the Docker Desktop, standalone and custom sockets apart: the not-running and timeout hints now name the standalone secrets engine when the socket is api.StandaloneSocketPath(), and keep the generic wording for any other socket.

Address review feedback: reword the permission hint to say the user lacks permission to connect and needs read and write access to the socket, quote the socket path so control characters cannot corrupt the output, and wrap PreRunE as well as RunE, as the keychain hints do.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Add ConnectError.Hint, which returns a one-line suggestion for fixing a failed connection based on its reason and socket: Docker Desktop's socket, the standalone engine's socket, or a custom one. Every SDK consumer can now show the same hints, and Error() is unchanged, so callers choose whether to print them.

pass's withEngineHint now only finds the ConnectError and appends its hint. The per-reason wording tests move to the client package.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
@Benehiko
Benehiko requested a review from joe0BAB October 8, 2026 13:29
@Benehiko
Benehiko enabled auto-merge October 8, 2026 13:31
Comment thread client/errors.go
permissionDeniedHint = "Your user lacks permission to connect to %s. Grant it read and write access to the socket and retry."
)

// Hint returns a user friendly message to help recover from the error.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

delete

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no

@Benehiko
Benehiko merged commit 4ccb9e2 into main Oct 8, 2026
20 checks passed
@Benehiko
Benehiko deleted the feat/pass-engine-hints branch October 8, 2026 13:39
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.

3 participants