Files
Alano Terblanche 0c2d0f700c vendor: github.com/docker/secrets-engine/client v0.2.2
Add the Docker secrets engine client, used to read the Docker Desktop session for Docker Hub. It brings in github.com/docker/secrets-engine/x v0.8.5 and connectrpc.com/connect v1.19.1.

The client module requires go1.26.10, which raises the go directive in vendor.mod from 1.26.0.

Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2026-10-22 11:10:40 +02:00

356 lines
10 KiB
Go

// Copyright 2025-2026 Docker, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"connectrpc.com/connect"
"github.com/docker/secrets-engine/client/dockerhub"
"github.com/docker/secrets-engine/x/api"
healthv1 "github.com/docker/secrets-engine/x/api/health/v1"
"github.com/docker/secrets-engine/x/api/health/v1/healthv1connect"
pluginsv1 "github.com/docker/secrets-engine/x/api/plugins/v1"
"github.com/docker/secrets-engine/x/api/plugins/v1/pluginsv1connect"
"github.com/docker/secrets-engine/x/api/resolver"
"github.com/docker/secrets-engine/x/secrets"
)
type (
Envelope = secrets.Envelope
ID = secrets.ID
Pattern = secrets.Pattern
AuthorizeResponse = secrets.AuthorizeResponse
DaemonVersion = api.DaemonVersion
)
var (
ParseID = secrets.ParseID
MustParseID = secrets.MustParseID
ParsePattern = secrets.ParsePattern
MustParsePattern = secrets.MustParsePattern
ErrSecretNotFound = secrets.ErrNotFound
ErrAccessDenied = secrets.ErrAccessDenied
)
var _ secrets.Resolver = &client{}
type Option func(c *config) error
func WithSocketPath(path string) Option {
return func(s *config) error {
if path == "" {
return errors.New("no path provided")
}
if s.dialContext != nil {
return errors.New("cannot set socket path and dial")
}
s.socketPath = path
s.dialContext = dialFromPath(path)
return nil
}
}
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Option {
return func(s *config) error {
if s.dialContext != nil {
return errors.New("cannot set socket path and dial")
}
s.dialContext = dialContext
return nil
}
}
// WithTimeout overrides the request timeout of the client.
//
// It is useful to set if there are hard-limits to when the client must wait
// for the server to accept the request.
//
// A timeout of 0 means no request timeout will be applied.
// Negative durations are not allowed and will result in an error.
func WithTimeout(timeout time.Duration) Option {
return func(s *config) error {
if timeout < 0 {
return errors.New("request timeout duration cannot be negative")
}
s.requestTimeout = timeout
return nil
}
}
// WithResponseTimeout overrides the response header timeout of the client.
// By default no timeout is applied, because a request may block on user
// interaction. 0 means no timeout; negative durations return an error.
func WithResponseTimeout(responseTimeout time.Duration) Option {
return func(s *config) error {
if responseTimeout < 0 {
return errors.New("response timeout duration cannot be negative")
}
s.responseTimeout = responseTimeout
return nil
}
}
type dial func(ctx context.Context, network, addr string) (net.Conn, error)
type config struct {
socketPath string
dialContext dial
requestTimeout time.Duration
responseTimeout time.Duration
}
var (
_ Client = &client{}
_ PluginManagement = &client{}
)
type client struct {
socketPath string
resolverClient secrets.Resolver
engineClient pluginsv1connect.PluginManagementServiceClient
versionClient healthv1connect.VersionServiceClient
authorizerClient secrets.Authorizer
}
// Authorize decides access to the patterns. The decision holds until the
// response expiry. A zero expiry means the decision never expires.
func (c client) Authorize(ctx context.Context, patterns ...secrets.Pattern) (secrets.AuthorizeResponse, error) {
resp, err := c.authorizerClient.Authorize(ctx, patterns...)
if isDialError(err) {
return secrets.AuthorizeResponse{}, c.connectError(err)
}
if err != nil {
return secrets.AuthorizeResponse{}, err
}
return resp, nil
}
func (c client) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secrets.Envelope, error) {
envelopes, err := c.resolverClient.GetSecrets(ctx, pattern)
if isDialError(err) {
return nil, c.connectError(err)
}
if err != nil {
return nil, err
}
return envelopes, nil
}
func (c client) HubAuth(opts ...dockerhub.Option) dockerhub.ClientAuth {
return dockerhub.New(c, opts...)
}
func (c client) Version(ctx context.Context) (DaemonVersion, error) {
resp, err := c.versionClient.GetVersion(ctx, connect.NewRequest(healthv1.GetVersionRequest_builder{}.Build()))
if isDialError(err) {
return DaemonVersion{}, c.connectError(err)
}
if err != nil {
return DaemonVersion{}, err
}
ver, err := api.NewVersion(resp.Msg.GetVersion())
if err != nil {
return DaemonVersion{}, fmt.Errorf("parsing daemon version %q: %w", resp.Msg.GetVersion(), err)
}
return DaemonVersion{Version: ver, Date: resp.Msg.GetDate(), CommitHash: resp.Msg.GetCommitHash()}, nil
}
// Client is the interface for interacting with the secrets engine daemon.
type Client interface {
secrets.Resolver
secrets.Authorizer
// Version returns the name and version reported by the daemon.
Version(ctx context.Context) (DaemonVersion, error)
// HubAuth returns a Docker Hub authentication accessor backed by this client.
HubAuth(opts ...dockerhub.Option) dockerhub.ClientAuth
}
type PluginManagement interface {
ListPlugins(ctx context.Context) ([]PluginInfo, error)
EnablePlugin(ctx context.Context, name string) error
DisablePlugin(ctx context.Context, name string) error
}
func PluginManagementFromClient(c Client) (PluginManagement, error) {
m, ok := c.(PluginManagement)
if !ok {
return nil, errors.New("client does not implement PluginManagement")
}
return m, nil
}
func isDialError(err error) bool {
if err == nil {
return false
}
var oe *net.OpError
if errors.As(err, &oe) && (oe.Op == "dial" || oe.Op == "connect") {
return true
}
return false
}
func (c client) connectError(err error) error {
if errors.Is(err, context.Canceled) {
return err
}
return &ConnectError{Reason: connectReason(err), SocketPath: c.socketPath, Err: err}
}
// New creates a client that connects to [api.StandaloneSocketPath] by default.
// To connect to Docker Desktop, use WithSocketPath(api.DesktopSocketPath()).
func New(options ...Option) (Client, error) {
cfg := &config{
requestTimeout: api.DefaultClientRequestTimeout,
responseTimeout: api.DefaultClientResponseHeaderTimeout,
}
for _, opt := range options {
if err := opt(cfg); err != nil {
return nil, err
}
}
if cfg.dialContext == nil {
cfg.socketPath = api.StandaloneSocketPath()
cfg.dialContext = dialFromPath(cfg.socketPath)
}
c := &http.Client{
Transport: &http.Transport{
// re-use the same connection to the runtime, this speeds up subsequent
// calls.
MaxConnsPerHost: api.DefaultClientMaxConnsPerHost,
MaxIdleConnsPerHost: api.DefaultClientMaxIdleConnsPerHost,
// keep the connection alive (good for long-lived clients)
IdleConnTimeout: api.DefaultClientIdleConnTimeout,
// no timeout by default; override with [WithResponseTimeout]
ResponseHeaderTimeout: cfg.responseTimeout,
TLSHandshakeTimeout: api.DefaultClientTLSHandshakeTimeout,
DialContext: cfg.dialContext,
DisableKeepAlives: false,
DisableCompression: false,
ForceAttemptHTTP2: true,
},
// by default Timeout will be 0 (meaning no timeout)
// it can be overwritten with [WithTimeout]
Timeout: cfg.requestTimeout,
}
return &client{
socketPath: cfg.socketPath,
resolverClient: resolver.NewResolverClient(c),
engineClient: pluginsv1connect.NewPluginManagementServiceClient(c, "http://unix"),
versionClient: healthv1connect.NewVersionServiceClient(c, "http://unix"),
authorizerClient: resolver.NewAuthorizerClient(c),
}, nil
}
func (c client) ListPlugins(ctx context.Context) ([]PluginInfo, error) {
req := connect.NewRequest(pluginsv1.ListPluginsRequest_builder{}.Build())
resp, err := c.engineClient.ListPlugins(ctx, req)
if isDialError(err) {
return nil, c.connectError(err)
}
if err != nil {
return nil, err
}
var result []PluginInfo
for _, item := range resp.Msg.GetPlugins() {
name, err := api.NewName(item.GetName())
if err != nil {
continue
}
version, err := api.NewVersion(item.GetVersion())
if err != nil {
continue
}
info := PluginInfo{
Name: name,
Version: version,
Disabled: item.GetDisabled(),
External: item.GetExternal(),
Configurable: item.GetConfigurable(),
RunStatus: item.GetRunStatus(),
StatusMessage: item.GetStatusMessage(),
}
switch item.WhichMetadata() {
case pluginsv1.Plugin_SecretsProvider_case:
pattern, err := secrets.ParsePattern(item.GetSecretsProvider().GetPattern())
if err != nil {
continue
}
info.SecretsProvider = &SecretsProviderMetadata{Pattern: pattern}
case pluginsv1.Plugin_AccessControlModule_case:
info.AccessControlModule = &AccessControlModuleMetadata{}
}
result = append(result, info)
}
return result, nil
}
func (c client) EnablePlugin(ctx context.Context, name string) error {
r := pluginsv1.EnablePluginRequest_builder{}.Build()
r.SetName(name)
_, err := c.engineClient.EnablePlugin(ctx, connect.NewRequest(r))
if isDialError(err) {
return c.connectError(err)
}
return err
}
func (c client) DisablePlugin(ctx context.Context, name string) error {
r := pluginsv1.DisablePluginRequest_builder{}.Build()
r.SetName(name)
_, err := c.engineClient.DisablePlugin(ctx, connect.NewRequest(r))
if isDialError(err) {
return c.connectError(err)
}
return err
}
type PluginInfo struct {
Name api.Name
Version api.Version
Disabled bool
External bool
Configurable bool
RunStatus pluginsv1.RunStatus
StatusMessage string
SecretsProvider *SecretsProviderMetadata
AccessControlModule *AccessControlModuleMetadata
}
type AccessControlModuleMetadata struct{}
type SecretsProviderMetadata struct {
Pattern secrets.Pattern
}
func dialFromPath(path string) dial {
return func(ctx context.Context, _, _ string) (net.Conn, error) {
d := &net.Dialer{}
return d.DialContext(ctx, "unix", path)
}
}