chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user