| //go:build go1.26 | |
| package secretsengine | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "io/fs" | |
| "os" | |
| "sync" | |
| "time" | |
| "github.com/docker/cli/cli/config/credentials" | |
| "github.com/docker/cli/cli/config/types" | |
| "github.com/docker/cli/internal/registry" | |
| seclient "github.com/docker/secrets-engine/client" | |
| "github.com/docker/secrets-engine/client/dockerhub" | |
| "github.com/docker/secrets-engine/x/api" | |
| "github.com/sirupsen/logrus" | |
| ) | |
| const ( | |
| lookupTimeout = 5 * time.Second | |
| sessionExpirySkew = 30 * time.Second | |
| sessionTTL = time.Minute | |
| retryInterval = time.Minute | |
| stagingRegistryHost = "registry-1-stage.docker.io" | |
| dhiRegistryHost = "dhi.io" | |
| ) | |
| var errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)") | |
| type hubClient interface { | |
| GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error) | |
| GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error) | |
| } | |
| var _ hubClient = dockerhub.ClientAuth(nil) | |
| type AuthProvider interface { | |
| GetAuthConfig(registryHostname string) (types.AuthConfig, error) | |
| } | |
| type provider struct { | |
| fallback AuthProvider | |
| hub *session | |
| staging *session | |
| } | |
| func NewDesktop(fallback AuthProvider) AuthProvider { | |
| return &provider{ | |
| fallback: fallback, | |
| hub: newDesktopSession("Docker Hub"), | |
| staging: newDesktopSession("Docker Hub staging", dockerhub.Staging()), | |
| } | |
| } | |
| func (p *provider) GetAuthConfig(registryHostname string) (types.AuthConfig, error) { | |
| if s, serverAddress := p.sessionFor(registryHostname); s != nil { | |
| if auth, ok := s.auth(); ok { | |
| auth.ServerAddress = serverAddress | |
| return auth, nil | |
| } | |
| } | |
| return p.fallback.GetAuthConfig(registryHostname) | |
| } | |
| func (p *provider) sessionFor(registryHostname string) (*session, string) { | |
| switch credentials.ConvertToHostname(registryHostname) { | |
| case registry.IndexName, registry.IndexHostname, registry.DefaultRegistryHost: | |
| return p.hub, registry.IndexServer | |
| case dhiRegistryHost: | |
| return p.hub, dhiRegistryHost | |
| case stagingRegistryHost: | |
| return p.staging, stagingRegistryHost | |
| default: | |
| return nil, "" | |
| } | |
| } | |
| type session struct { | |
| name string | |
| connect func() (hubClient, error) | |
| mu sync.Mutex | |
| hub hubClient | |
| retryAt time.Time | |
| cached *types.AuthConfig | |
| validUntil time.Time | |
| } | |
| func newDesktopSession(name string, opts ...dockerhub.Option) *session { | |
| return &session{ | |
| name: name, | |
| connect: func() (hubClient, error) { | |
| return connect(api.DesktopSocketPath(), opts...) | |
| }, | |
| } | |
| } | |
| func connect(socketPath string, opts ...dockerhub.Option) (hubClient, error) { | |
| if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) { | |
| return nil, err | |
| } | |
| c, err := seclient.New( | |
| seclient.WithSocketPath(socketPath), | |
| seclient.WithTimeout(lookupTimeout), | |
| ) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return c.HubAuth(opts...), nil | |
| } | |
| func (s *session) auth() (types.AuthConfig, bool) { | |
| s.mu.Lock() | |
| defer s.mu.Unlock() | |
| now := time.Now() | |
| if now.Before(s.retryAt) { | |
| return types.AuthConfig{}, false | |
| } | |
| if s.cached != nil && now.Before(s.validUntil) { | |
| return *s.cached, true | |
| } | |
| auth, validUntil, err := s.fetch() | |
| if err != nil { | |
| s.retryAt = time.Now().Add(retryInterval) | |
| s.cached = nil | |
| logFallback(s.name, err) | |
| return types.AuthConfig{}, false | |
| } | |
| logrus.Debugf("using the %s session of %q from Docker Desktop", s.name, auth.Username) | |
| s.cached, s.validUntil = &auth, validUntil | |
| return auth, true | |
| } | |
| func (s *session) fetch() (types.AuthConfig, time.Time, error) { | |
| if s.hub == nil { | |
| hub, err := s.connect() | |
| if err != nil { | |
| return types.AuthConfig{}, time.Time{}, err | |
| } | |
| s.hub = hub | |
| } | |
| ctx, cancel := context.WithTimeout(context.Background(), lookupTimeout) | |
| defer cancel() | |
| userSession, err := s.hub.GetDefaultSession(ctx) | |
| if err != nil { | |
| return types.AuthConfig{}, time.Time{}, err | |
| } | |
| username := userSession.Claims.Username | |
| if username == "" { | |
| profile, err := s.hub.GetDefaultProfile(ctx) | |
| if err != nil { | |
| return types.AuthConfig{}, time.Time{}, fmt.Errorf("resolving the Docker Hub username: %w", err) | |
| } | |
| username = profile.Username | |
| } | |
| if username == "" { | |
| return types.AuthConfig{}, time.Time{}, errors.New("no username for the session") | |
| } | |
| now := time.Now() | |
| validUntil := now.Add(sessionTTL) | |
| if exp := userSession.Claims.ExpiresAt; exp != nil { | |
| validUntil = exp.Add(-sessionExpirySkew) | |
| if !now.Before(validUntil) { | |
| return types.AuthConfig{}, time.Time{}, errSessionExpired | |
| } | |
| } | |
| return types.AuthConfig{ | |
| Username: username, | |
| Password: userSession.AccessToken, | |
| }, validUntil, nil | |
| } | |
| func logFallback(name string, err error) { | |
| if isSessionUnavailable(err) { | |
| logrus.Debugf("no %s session available from Docker Desktop, using Docker CLI credentials: %v", name, err) | |
| return | |
| } | |
| logrus.Warnf("could not use the %s session from Docker Desktop, using Docker CLI credentials instead: %v", name, err) | |
| } | |
| func isSessionUnavailable(err error) bool { | |
| if _, ok := errors.AsType[*seclient.ConnectError](err); ok { | |
| return true | |
| } | |
| return errors.Is(err, fs.ErrNotExist) || | |
| errors.Is(err, dockerhub.ErrNoSession) || | |
| errors.Is(err, errSessionExpired) | |
| } |