87 lines
2.8 KiB
Go
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)
|
|
}
|