Files
portainer/pkg/libstack/dockercli.go
2026-05-14 10:13:19 +12:00

87 lines
2.8 KiB
Go

package libstack
import (
"context"
"fmt"
"sync"
"github.com/portainer/portainer/api/logs"
"github.com/docker/cli/cli/command"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/flags"
"github.com/docker/docker/registry"
"github.com/rs/zerolog/log"
)
// DockerCliOptions holds the settings required to initialise a DockerCli.
type DockerCliOptions struct {
Host string
Registries []configtypes.AuthConfig
}
// mu serialises calls to cli.Initialize across all deployer types (compose and
// swarm) to prevent concurrent initialisation of the Docker client config.
var mu sync.Mutex
// WithCli creates and initialises a DockerCli, injects registry credentials,
// and calls cliFn with the ready client. The client is closed after cliFn returns.
func WithCli(
ctx context.Context, //nolint:staticcheck
options DockerCliOptions,
cliFn func(context.Context, *command.DockerCli) error,
) error {
ctx = context.Background() //nolint:staticcheck
cli, err := command.NewDockerCli(command.WithCombinedStreams(log.Logger))
if err != nil {
return fmt.Errorf("unable to create a Docker client: %w", err)
}
opts := flags.NewClientOptions()
if options.Host != "" {
opts.Hosts = []string{options.Host}
}
mu.Lock()
if err := cli.Initialize(opts); err != nil {
mu.Unlock()
return fmt.Errorf("unable to initialize the Docker client: %w", err)
}
mu.Unlock()
defer logs.CloseAndLogErr(cli.Client())
for _, r := range options.Registries {
if r.ServerAddress == "" || r.ServerAddress == registry.DefaultNamespace {
r.ServerAddress = registry.IndexServer
}
cli.ConfigFile().AuthConfigs[r.ServerAddress] = r
}
// Docker resolves credentials in the following priority:
// 1. credHelpers – per-registry credential helpers
// 2. credsStore – global credential store used for all registries
// 3. auths – inline credentials defined in config.json
//
// Many Docker Desktop users (Windows/macOS) have a global credsStore configured
// by default (e.g. "desktop.exe" on Windows or "osxkeychain" on macOS). These
// global stores often do not include credentials for the custom registries
// defined in Portainer stacks, leading to authentication failures.
//
// To avoid this, when inline credentials are provided for one or more registries,
// we intentionally clear the global credsStore. This ensures Docker uses the
// credentials configured in Portainer instead of falling back to an empty global
// store.
//
// If no inline credentials are configured in Portainer, we keep the credsStore
// so Docker can still use it as a fallback.
// credHelpers are not affected as they are external services managed by the user.
// @ref: https://linear.app/portainer/issue/BE-12237
if len(options.Registries) > 0 {
cli.ConfigFile().CredentialsStore = ""
}
return cliFn(ctx, cli)
}