Files
2026-07-13 12:08:39 +08:00

101 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package libstack
import (
"context"
"fmt"
"maps"
"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
// Headers are injected as custom HTTP headers on every request the client makes.
Headers map[string]string
}
// 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()
// Inject custom headers before the first Client() call, which is where the
// Docker client is lazily created and reads ConfigFile().HTTPHeaders.
if len(options.Headers) > 0 {
cfg := cli.ConfigFile()
if cfg.HTTPHeaders == nil {
cfg.HTTPHeaders = make(map[string]string)
}
maps.Copy(cfg.HTTPHeaders, options.Headers)
}
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)
}