blob: 61ea71e409bf469e4397bfaed9a835a3a558103d [file] [edit]
//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)
}