mirror of
https://github.com/docker/cli.git
synced 2026-11-06 13:10:41 -04:00
cli/command: test credential precedence with Docker Desktop
Check that the Docker Desktop session is used for Docker Hub and dhi.io, that other registries use the configuration file, and that DOCKER_AUTH_CONFIG takes precedence over Docker Desktop. TestRetrieveAuthTokenFromImage disables the Docker Desktop lookup. Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
@@ -133,7 +133,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), img)
|
||||
encodedAuth, err := command.EncodedRegistryAuth(dockerCLI, img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error
|
||||
ociPlatforms = append(ociPlatforms, p)
|
||||
}
|
||||
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), distributionRef.String())
|
||||
encodedAuth, err := command.EncodedRegistryAuth(dockerCLI, distributionRef.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
}
|
||||
|
||||
// Resolve the Auth config relevant for this server
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), ref.String())
|
||||
encodedAuth, err := command.EncodedRegistryAuth(dockerCli, ref.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func buildPullConfig(dockerCLI command.Cli, opts pluginOptions) (client.PluginIn
|
||||
return client.PluginInstallOptions{}, err
|
||||
}
|
||||
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), ref.String())
|
||||
encodedAuth, err := command.EncodedRegistryAuth(dockerCLI, ref.String())
|
||||
if err != nil {
|
||||
return client.PluginInstallOptions{}, err
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, name string) error {
|
||||
}
|
||||
|
||||
named = reference.TagNameOnly(named)
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), named.String())
|
||||
encodedAuth, err := command.EncodedRegistryAuth(dockerCli, named.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+20
-6
@@ -36,8 +36,6 @@ const (
|
||||
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
|
||||
const authConfigKey = "https://index.docker.io/v1/"
|
||||
|
||||
var desktopAuthConfig = secretsengine.AuthConfig
|
||||
|
||||
func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) {
|
||||
if os.Getenv(configfile.DockerEnvConfigKey) == "" {
|
||||
return configtypes.AuthConfig{}, false
|
||||
@@ -206,6 +204,25 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
|
||||
//
|
||||
// [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
|
||||
func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) {
|
||||
return retrieveAuthTokenFromImage(secretsengine.NewDesktop(cfg), image)
|
||||
}
|
||||
|
||||
type registryAuthProvider interface {
|
||||
RegistryAuthProvider() secretsengine.AuthProvider
|
||||
}
|
||||
|
||||
func EncodedRegistryAuth(dockerCLI Cli, image string) (string, error) {
|
||||
if p, ok := dockerCLI.(registryAuthProvider); ok {
|
||||
return retrieveAuthTokenFromImage(p.RegistryAuthProvider(), image)
|
||||
}
|
||||
return RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
||||
return secretsengine.NewDesktop(cli.ConfigFile())
|
||||
}
|
||||
|
||||
func retrieveAuthTokenFromImage(auth secretsengine.AuthProvider, image string) (string, error) {
|
||||
registryRef, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -213,10 +230,7 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
domain := reference.Domain(registryRef)
|
||||
authConfig, ok := envAuthConfig(domain)
|
||||
if !ok {
|
||||
authConfig, ok = desktopAuthConfig(domain)
|
||||
}
|
||||
if !ok {
|
||||
authConfig, err = cfg.GetAuthConfig(domain)
|
||||
authConfig, err = auth.GetAuthConfig(domain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/moby/moby/api/pkg/authconfig"
|
||||
registrytypes "github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type recordingAuthProvider struct {
|
||||
hosts []string
|
||||
}
|
||||
|
||||
func (p *recordingAuthProvider) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
|
||||
p.hosts = append(p.hosts, registryHostname)
|
||||
return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, nil
|
||||
}
|
||||
|
||||
func TestRetrieveAuthTokenFromImageUsesAuthProvider(t *testing.T) {
|
||||
auth := &recordingAuthProvider{}
|
||||
|
||||
actual, err := retrieveAuthTokenFromImage(auth, "dhi.io/python")
|
||||
assert.NilError(t, err)
|
||||
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(actual, expected))
|
||||
assert.Check(t, is.DeepEqual(auth.hosts, []string{"dhi.io"}))
|
||||
}
|
||||
|
||||
func TestRetrieveAuthTokenFromImageEnvTakesPrecedence(t *testing.T) {
|
||||
t.Setenv(configfile.DockerEnvConfigKey, `{"auths":{"https://index.docker.io/v1/":{"auth":"ZW52dXNlcjplbnZwYXNz"}}}`)
|
||||
auth := &recordingAuthProvider{}
|
||||
|
||||
actual, err := retrieveAuthTokenFromImage(auth, "ubuntu")
|
||||
assert.NilError(t, err)
|
||||
expected, err := authconfig.Encode(registrytypes.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "https://index.docker.io/v1/"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(actual, expected))
|
||||
assert.Check(t, is.Len(auth.hosts, 0), "the auth provider must not be asked for a registry in DOCKER_AUTH_CONFIG")
|
||||
}
|
||||
|
||||
func TestRetrieveAuthTokenFromImage(t *testing.T) {
|
||||
// configFileContent contains a plain-text "username:password", as stored by
|
||||
// the plain-text store;
|
||||
// https://github.com/docker/cli/blob/v28.0.4/cli/config/configfile/file.go#L218-L229
|
||||
const configFileContent = `{"auths": {
|
||||
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry-1.docker.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
|
||||
}
|
||||
}`
|
||||
cfg := configfile.ConfigFile{}
|
||||
err := cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent)))
|
||||
assert.NilError(t, err)
|
||||
|
||||
remoteRefs := []string{
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
"ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu",
|
||||
"library/ubuntu:latest",
|
||||
"library/ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
prefix string
|
||||
expectedAddress string
|
||||
expectedAuthCfg registrytypes.AuthConfig
|
||||
}{
|
||||
{
|
||||
prefix: "",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "index.docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry-1.docker.io (the actual registry) is the odd one out, and is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry-1.docker.io",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry-1.docker.io"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry.hub.docker.com is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry.hub.docker.com",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.hub.docker.com"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]",
|
||||
expectedAddress: "[::1]",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]:5000",
|
||||
expectedAddress: "[::1]:5000",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "127.0.0.1",
|
||||
expectedAddress: "127.0.0.1",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "127.0.0.1"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost",
|
||||
expectedAddress: "localhost",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost:5000",
|
||||
expectedAddress: "localhost:5000",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "no-auth.example.com",
|
||||
expectedAuthCfg: registrytypes.AuthConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tcName := tc.prefix
|
||||
if tc.prefix == "" {
|
||||
tcName = "no-prefix"
|
||||
}
|
||||
t.Run(tcName, func(t *testing.T) {
|
||||
for _, remoteRef := range remoteRefs {
|
||||
imageRef := path.Join(tc.prefix, remoteRef)
|
||||
actual, err := retrieveAuthTokenFromImage(&cfg, imageRef)
|
||||
assert.NilError(t, err)
|
||||
expectedAuthCfg, err := authconfig.Encode(tc.expectedAuthCfg)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, actual, expectedAuthCfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+30
-112
@@ -1,14 +1,13 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/internal/secretsengine"
|
||||
"github.com/moby/moby/api/pkg/authconfig"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -82,6 +81,35 @@ func TestGetDefaultAuthConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthProvider struct {
|
||||
hosts []string
|
||||
}
|
||||
|
||||
func (p *fakeAuthProvider) GetAuthConfig(registryHostname string) (configtypes.AuthConfig, error) {
|
||||
p.hosts = append(p.hosts, registryHostname)
|
||||
return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, nil
|
||||
}
|
||||
|
||||
type authProviderCli struct {
|
||||
command.Cli
|
||||
auth *fakeAuthProvider
|
||||
}
|
||||
|
||||
func (c *authProviderCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
||||
return c.auth
|
||||
}
|
||||
|
||||
func TestEncodedRegistryAuthUsesCLIAuthProvider(t *testing.T) {
|
||||
dockerCLI := &authProviderCli{auth: &fakeAuthProvider{}}
|
||||
|
||||
actual, err := command.EncodedRegistryAuth(dockerCLI, "dhi.io/python")
|
||||
assert.NilError(t, err)
|
||||
expected, err := authconfig.Encode(registry.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(actual, expected))
|
||||
assert.Check(t, is.DeepEqual(dockerCLI.auth.hosts, []string{"dhi.io"}))
|
||||
}
|
||||
|
||||
func TestGetDefaultAuthConfig_HelperError(t *testing.T) {
|
||||
cfg := configfile.New("filename")
|
||||
cfg.CredentialsStore = "fake-does-not-exist"
|
||||
@@ -95,113 +123,3 @@ func TestGetDefaultAuthConfig_HelperError(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(expectedAuthConfig, authCfg))
|
||||
assert.Check(t, is.ErrorContains(err, "docker-credential-fake-does-not-exist"))
|
||||
}
|
||||
|
||||
func TestRetrieveAuthTokenFromImage(t *testing.T) {
|
||||
// configFileContent contains a plain-text "username:password", as stored by
|
||||
// the plain-text store;
|
||||
// https://github.com/docker/cli/blob/v28.0.4/cli/config/configfile/file.go#L218-L229
|
||||
const configFileContent = `{"auths": {
|
||||
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry-1.docker.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
|
||||
}
|
||||
}`
|
||||
cfg := configfile.ConfigFile{}
|
||||
err := cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent)))
|
||||
assert.NilError(t, err)
|
||||
|
||||
remoteRefs := []string{
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
"ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu",
|
||||
"library/ubuntu:latest",
|
||||
"library/ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
prefix string
|
||||
expectedAddress string
|
||||
expectedAuthCfg registry.AuthConfig
|
||||
}{
|
||||
{
|
||||
prefix: "",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "index.docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry-1.docker.io (the actual registry) is the odd one out, and is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry-1.docker.io",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry-1.docker.io"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry.hub.docker.com is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry.hub.docker.com",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.hub.docker.com"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]",
|
||||
expectedAddress: "[::1]",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]:5000",
|
||||
expectedAddress: "[::1]:5000",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "127.0.0.1",
|
||||
expectedAddress: "127.0.0.1",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "127.0.0.1"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost",
|
||||
expectedAddress: "localhost",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost:5000",
|
||||
expectedAddress: "localhost:5000",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "no-auth.example.com",
|
||||
expectedAuthCfg: registry.AuthConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tcName := tc.prefix
|
||||
if tc.prefix == "" {
|
||||
tcName = "no-prefix"
|
||||
}
|
||||
t.Run(tcName, func(t *testing.T) {
|
||||
for _, remoteRef := range remoteRefs {
|
||||
imageRef := path.Join(tc.prefix, remoteRef)
|
||||
actual, err := command.RetrieveAuthTokenFromImage(&cfg, imageRef)
|
||||
assert.NilError(t, err)
|
||||
expectedAuthCfg, err := authconfig.Encode(tc.expectedAuthCfg)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, actual, expectedAuthCfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
if opts.registryAuth {
|
||||
// Retrieve encoded auth token from the image reference
|
||||
var err error
|
||||
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), opts.image)
|
||||
encodedAuth, err = command.EncodedRegistryAuth(dockerCLI, opts.image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
// Retrieve encoded auth token from the image reference
|
||||
// This would be the old image if it didn't change in this update
|
||||
var err error
|
||||
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), spec.TaskTemplate.ContainerSpec.Image)
|
||||
encodedAuth, err = command.EncodedRegistryAuth(dockerCLI, spec.TaskTemplate.ContainerSpec.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func deployServices(ctx context.Context, dockerCLI command.Cli, services map[str
|
||||
|
||||
if sendAuth {
|
||||
// Retrieve encoded auth token from the image reference
|
||||
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image)
|
||||
encodedAuth, err = command.EncodedRegistryAuth(dockerCLI, image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ const (
|
||||
// IndexServer is used for user auth and image search
|
||||
IndexServer = "https://index.docker.io/v1/"
|
||||
// IndexName is the name of the index
|
||||
IndexName = "docker.io"
|
||||
IndexName = "docker.io"
|
||||
DefaultRegistryHost = "registry-1.docker.io"
|
||||
StagingRegistryHost = "registry-1-stage.docker.io"
|
||||
DHIRegistryHost = "dhi.io"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -61,7 +64,7 @@ var (
|
||||
// or "ubuntu:latest"), as well as references using the "docker.io" domain
|
||||
// name, which is used as canonical reference for images on Docker Hub, but
|
||||
// does not match the domain-name of Docker Hub's registry.
|
||||
DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"}
|
||||
DefaultV2Registry = &url.URL{Scheme: "https", Host: DefaultRegistryHost}
|
||||
|
||||
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
//go:build go1.26
|
||||
|
||||
package secretsengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
seclient "github.com/docker/secrets-engine/client"
|
||||
"github.com/docker/secrets-engine/client/dockerhub"
|
||||
"github.com/docker/secrets-engine/x/api/resolver"
|
||||
"github.com/docker/secrets-engine/x/api/resolver/v1/resolverv1connect"
|
||||
"github.com/docker/secrets-engine/x/secrets"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
const benchSocketEnv = "SECRETSENGINE_BENCH_SOCKET"
|
||||
|
||||
type fakeResolver struct {
|
||||
latency time.Duration
|
||||
values map[string][]byte
|
||||
calls atomic.Int64
|
||||
}
|
||||
|
||||
func (r *fakeResolver) GetSecrets(ctx context.Context, pattern secrets.Pattern) ([]secrets.Envelope, error) {
|
||||
r.calls.Add(1)
|
||||
if r.latency > 0 {
|
||||
select {
|
||||
case <-time.After(r.latency):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
var envelopes []secrets.Envelope
|
||||
for key, value := range r.values {
|
||||
id := secrets.MustParseID(key)
|
||||
if pattern.Match(id) {
|
||||
envelopes = append(envelopes, secrets.Envelope{ID: id, Value: value, ResolvedAt: time.Now()})
|
||||
}
|
||||
}
|
||||
if len(envelopes) == 0 {
|
||||
return nil, secrets.ErrNotFound
|
||||
}
|
||||
return envelopes, nil
|
||||
}
|
||||
|
||||
func fakeAccessToken() string {
|
||||
enc := base64.RawURLEncoding.EncodeToString
|
||||
return enc([]byte(`{"alg":"RS256","typ":"JWT"}`)) + "." + enc([]byte(`{"sub":"bench"}`)) + "." + enc([]byte("signature"))
|
||||
}
|
||||
|
||||
func signedIn(tb testing.TB, expiresAt time.Time) map[string][]byte {
|
||||
tb.Helper()
|
||||
const username = "hubuser"
|
||||
userID := "docker/auth/hub/" + username
|
||||
profile, err := json.Marshal(dockerhub.Profile{UserID: userID, Username: username, Email: username + "@example.com"})
|
||||
assert.NilError(tb, err)
|
||||
s := dockerhub.UserSession{AccessToken: fakeAccessToken()}
|
||||
s.Claims.Username = username
|
||||
s.Claims.ExpiresAt = &dockerhub.NumericDate{Time: expiresAt}
|
||||
userSession, err := json.Marshal(s)
|
||||
assert.NilError(tb, err)
|
||||
return map[string][]byte{
|
||||
"docker/auth/metadata/hub/default": profile,
|
||||
userID: userSession,
|
||||
}
|
||||
}
|
||||
|
||||
var socketDirs atomic.Int64
|
||||
|
||||
func socketDir(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
dir := filepath.Join(os.TempDir(), fmt.Sprintf("se-%d-%d", os.Getpid(), socketDirs.Add(1)))
|
||||
assert.NilError(tb, os.Mkdir(dir, 0o700))
|
||||
tb.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
return dir
|
||||
}
|
||||
|
||||
type fakeEngine struct {
|
||||
socketPath string
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[net.Conn]struct{}
|
||||
}
|
||||
|
||||
func startEngine(tb testing.TB, r *fakeResolver) *fakeEngine {
|
||||
tb.Helper()
|
||||
e := &fakeEngine{
|
||||
socketPath: filepath.Join(socketDir(tb), "engine.sock"),
|
||||
conns: map[net.Conn]struct{}{},
|
||||
}
|
||||
ln, err := net.Listen("unix", e.socketPath)
|
||||
assert.NilError(tb, err)
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(resolverv1connect.NewResolverServiceHandler(resolver.NewResolverHandler(r)))
|
||||
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, ConnState: e.track}
|
||||
go func() { _ = srv.Serve(ln) }()
|
||||
tb.Cleanup(func() { _ = srv.Close() })
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *fakeEngine) track(c net.Conn, state http.ConnState) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
switch state {
|
||||
case http.StateNew:
|
||||
e.conns[c] = struct{}{}
|
||||
case http.StateClosed, http.StateHijacked:
|
||||
delete(e.conns, c)
|
||||
case http.StateActive, http.StateIdle:
|
||||
}
|
||||
}
|
||||
|
||||
func (e *fakeEngine) dropConnections() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for c := range e.conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
clear(e.conns)
|
||||
}
|
||||
|
||||
func startHungEngine(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
socketPath := filepath.Join(socketDir(tb), "engine.sock")
|
||||
ln, err := net.Listen("unix", socketPath)
|
||||
assert.NilError(tb, err)
|
||||
var mu sync.Mutex
|
||||
var held []net.Conn
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
held = append(held, c)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
tb.Cleanup(func() {
|
||||
_ = ln.Close()
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, c := range held {
|
||||
_ = c.Close()
|
||||
}
|
||||
})
|
||||
return socketPath
|
||||
}
|
||||
|
||||
func staleSocket(tb testing.TB) string {
|
||||
tb.Helper()
|
||||
socketPath := filepath.Join(socketDir(tb), "engine.sock")
|
||||
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})
|
||||
assert.NilError(tb, err)
|
||||
ln.SetUnlinkOnClose(false)
|
||||
assert.NilError(tb, ln.Close())
|
||||
_, err = os.Lstat(socketPath)
|
||||
assert.NilError(tb, err)
|
||||
return socketPath
|
||||
}
|
||||
|
||||
func socketSession(socketPath string, timeout time.Duration) *session {
|
||||
return &session{
|
||||
name: "Docker Hub",
|
||||
connect: func() (hubSessions, error) {
|
||||
return connect(socketPath)
|
||||
},
|
||||
timeout: timeout,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineSignedIn(t *testing.T) {
|
||||
r := &fakeResolver{values: signedIn(t, time.Now().Add(time.Hour))}
|
||||
e := startEngine(t, r)
|
||||
p := &provider{hub: socketSession(e.socketPath, lookupTimeout)}
|
||||
|
||||
got, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, got.Username, "hubuser")
|
||||
assert.Equal(t, got.Password, fakeAccessToken())
|
||||
assert.Equal(t, got.ServerAddress, indexServer)
|
||||
assert.Equal(t, got.IdentityToken, "")
|
||||
|
||||
got, ok = p.authConfig("dhi.io")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, got.ServerAddress, dhiRegistryHost)
|
||||
assert.Equal(t, r.calls.Load(), int64(2), "the profile and the session are read once, then cached")
|
||||
}
|
||||
|
||||
func TestEngineSignedOut(t *testing.T) {
|
||||
r := &fakeResolver{}
|
||||
e := startEngine(t, r)
|
||||
p := &provider{hub: socketSession(e.socketPath, lookupTimeout)}
|
||||
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, r.calls.Load(), int64(1))
|
||||
}
|
||||
|
||||
func TestMissingSocketFallsBackImmediately(t *testing.T) {
|
||||
s := socketSession(filepath.Join(socketDir(t), "engine.sock"), lookupTimeout)
|
||||
|
||||
start := time.Now()
|
||||
_, ok := s.auth()
|
||||
assert.Assert(t, !ok)
|
||||
assert.Assert(t, time.Since(start) < time.Second)
|
||||
assert.Assert(t, s.hub == nil, "no client is created without a socket")
|
||||
}
|
||||
|
||||
func TestStaleSocketFallsBackImmediately(t *testing.T) {
|
||||
s := socketSession(staleSocket(t), lookupTimeout)
|
||||
|
||||
start := time.Now()
|
||||
_, ok := s.auth()
|
||||
assert.Assert(t, !ok)
|
||||
assert.Assert(t, time.Since(start) < time.Second)
|
||||
}
|
||||
|
||||
func TestHungEngineIsBounded(t *testing.T) {
|
||||
const timeout = 200 * time.Millisecond
|
||||
s := socketSession(startHungEngine(t), timeout)
|
||||
|
||||
start := time.Now()
|
||||
_, ok := s.auth()
|
||||
elapsed := time.Since(start)
|
||||
assert.Assert(t, !ok)
|
||||
assert.Assert(t, elapsed >= timeout, elapsed)
|
||||
assert.Assert(t, elapsed < timeout+2*time.Second, elapsed)
|
||||
}
|
||||
|
||||
func benchmarkColdLookup(b *testing.B, socketPath string, e *fakeEngine, want bool) {
|
||||
b.Helper()
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
s := socketSession(socketPath, lookupTimeout)
|
||||
if _, ok := s.auth(); ok != want {
|
||||
b.Fatalf("got a Desktop session: %t, want %t", ok, want)
|
||||
}
|
||||
if e != nil {
|
||||
b.StopTimer()
|
||||
e.dropConnections()
|
||||
b.StartTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAuthConfig(b *testing.B) {
|
||||
b.Run("other-registry", func(b *testing.B) {
|
||||
p := &provider{hub: socketSession(filepath.Join(socketDir(b), "engine.sock"), lookupTimeout)}
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
if _, ok := p.authConfig("ghcr.io"); ok {
|
||||
b.Fatal("unexpected Desktop session")
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run("missing-socket", func(b *testing.B) {
|
||||
benchmarkColdLookup(b, filepath.Join(socketDir(b), "engine.sock"), nil, false)
|
||||
})
|
||||
b.Run("missing-socket-without-stat", func(b *testing.B) {
|
||||
socketPath := filepath.Join(socketDir(b), "engine.sock")
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
c, err := seclient.New(seclient.WithSocketPath(socketPath), seclient.WithTimeout(lookupTimeout))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, err := c.HubAuth().GetDefaultSession(context.Background()); err == nil {
|
||||
b.Fatal("unexpected Desktop session")
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run("stale-socket", func(b *testing.B) {
|
||||
benchmarkColdLookup(b, staleSocket(b), nil, false)
|
||||
})
|
||||
b.Run("signed-out", func(b *testing.B) {
|
||||
e := startEngine(b, &fakeResolver{})
|
||||
benchmarkColdLookup(b, e.socketPath, e, false)
|
||||
})
|
||||
b.Run("signed-in", func(b *testing.B) {
|
||||
e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))})
|
||||
benchmarkColdLookup(b, e.socketPath, e, true)
|
||||
})
|
||||
b.Run("signed-in-10ms-engine", func(b *testing.B) {
|
||||
e := startEngine(b, &fakeResolver{latency: 10 * time.Millisecond, values: signedIn(b, time.Now().Add(time.Hour))})
|
||||
benchmarkColdLookup(b, e.socketPath, e, true)
|
||||
})
|
||||
b.Run("signed-in-refetch", func(b *testing.B) {
|
||||
e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))})
|
||||
s := socketSession(e.socketPath, lookupTimeout)
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Desktop session")
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
s.cached = nil
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Desktop session")
|
||||
}
|
||||
}
|
||||
})
|
||||
b.Run("signed-in-cached", func(b *testing.B) {
|
||||
e := startEngine(b, &fakeResolver{values: signedIn(b, time.Now().Add(time.Hour))})
|
||||
s := socketSession(e.socketPath, lookupTimeout)
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Desktop session")
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Desktop session")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type trackingDialer struct {
|
||||
mu sync.Mutex
|
||||
conns []net.Conn
|
||||
}
|
||||
|
||||
func (d *trackingDialer) dial(socketPath string) func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var nd net.Dialer
|
||||
c, err := nd.DialContext(ctx, "unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.conns = append(d.conns, c)
|
||||
d.mu.Unlock()
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *trackingDialer) closeAll() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for _, c := range d.conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
d.conns = nil
|
||||
}
|
||||
|
||||
func BenchmarkExternalEngine(b *testing.B) {
|
||||
socketPath := os.Getenv(benchSocketEnv)
|
||||
if socketPath == "" {
|
||||
b.Skipf("%s is not set", benchSocketEnv)
|
||||
}
|
||||
b.Run("cold", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
var dialer trackingDialer
|
||||
s := &session{
|
||||
name: "Docker Hub",
|
||||
connect: func() (hubSessions, error) {
|
||||
c, err := seclient.New(seclient.WithDialContext(dialer.dial(socketPath)), seclient.WithTimeout(lookupTimeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HubAuth(), nil
|
||||
},
|
||||
timeout: lookupTimeout,
|
||||
now: time.Now,
|
||||
}
|
||||
_, ok := s.auth()
|
||||
b.StopTimer()
|
||||
dialer.closeAll()
|
||||
if !ok {
|
||||
b.Fatal("no Docker Hub session from the engine at " + socketPath)
|
||||
}
|
||||
b.StartTimer()
|
||||
}
|
||||
})
|
||||
b.Run("refetch", func(b *testing.B) {
|
||||
s := socketSession(socketPath, lookupTimeout)
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Docker Hub session from the engine at " + socketPath)
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
s.cached = nil
|
||||
if _, ok := s.auth(); !ok {
|
||||
b.Fatal("no Docker Hub session from the engine at " + socketPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"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"
|
||||
@@ -25,10 +25,6 @@ const (
|
||||
sessionExpirySkew = 30 * time.Second
|
||||
sessionTTL = time.Minute
|
||||
retryInterval = time.Minute
|
||||
|
||||
indexServer = "https://index.docker.io/v1/"
|
||||
dhiRegistryHost = "dhi.io"
|
||||
stagingRegistryHost = "registry-1-stage.docker.io"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,57 +32,62 @@ var (
|
||||
errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)")
|
||||
)
|
||||
|
||||
type hubSessions interface {
|
||||
type HubClient interface {
|
||||
GetDefaultSession(ctx context.Context) (dockerhub.UserSession, error)
|
||||
GetDefaultProfile(ctx context.Context) (dockerhub.Profile, error)
|
||||
}
|
||||
|
||||
var _ hubSessions = dockerhub.ClientAuth(nil)
|
||||
var _ HubClient = dockerhub.ClientAuth(nil)
|
||||
|
||||
type provider struct {
|
||||
hub *session
|
||||
staging *session
|
||||
type AuthProvider interface {
|
||||
GetAuthConfig(registryHostname string) (types.AuthConfig, error)
|
||||
}
|
||||
|
||||
var shared = &provider{
|
||||
hub: newSession("Docker Hub"),
|
||||
staging: newSession("Docker Hub staging", dockerhub.Staging()),
|
||||
type Provider struct {
|
||||
fallback AuthProvider
|
||||
hub *session
|
||||
staging *session
|
||||
}
|
||||
|
||||
var disabled atomic.Bool
|
||||
|
||||
func Disable() {
|
||||
disabled.Store(true)
|
||||
}
|
||||
|
||||
func AuthConfig(registryHostname string) (types.AuthConfig, bool) {
|
||||
if disabled.Load() {
|
||||
return types.AuthConfig{}, false
|
||||
func New(fallback AuthProvider, hub, staging HubClient) *Provider {
|
||||
return &Provider{
|
||||
fallback: fallback,
|
||||
hub: newSession("Docker Hub", hub),
|
||||
staging: newSession("Docker Hub staging", staging),
|
||||
}
|
||||
return shared.authConfig(registryHostname)
|
||||
}
|
||||
|
||||
func (p *provider) authConfig(registryHostname string) (types.AuthConfig, bool) {
|
||||
s, serverAddress := p.sessionFor(registryHostname)
|
||||
if s == nil {
|
||||
return types.AuthConfig{}, false
|
||||
var (
|
||||
desktopHubSession = newDesktopSession("Docker Hub")
|
||||
desktopStagingSession = newDesktopSession("Docker Hub staging", dockerhub.Staging())
|
||||
)
|
||||
|
||||
func NewDesktop(fallback AuthProvider) *Provider {
|
||||
return &Provider{
|
||||
fallback: fallback,
|
||||
hub: desktopHubSession,
|
||||
staging: desktopStagingSession,
|
||||
}
|
||||
auth, ok := s.auth()
|
||||
if !ok {
|
||||
return types.AuthConfig{}, false
|
||||
}
|
||||
auth.ServerAddress = serverAddress
|
||||
return auth, true
|
||||
}
|
||||
|
||||
func (p *provider) sessionFor(registryHostname string) (*session, string) {
|
||||
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 "docker.io", "index.docker.io", "registry-1.docker.io":
|
||||
return p.hub, indexServer
|
||||
case dhiRegistryHost:
|
||||
return p.hub, dhiRegistryHost
|
||||
case stagingRegistryHost:
|
||||
return p.staging, stagingRegistryHost
|
||||
case registry.IndexName, registry.IndexHostname, registry.DefaultRegistryHost:
|
||||
return p.hub, registry.IndexServer
|
||||
case registry.DHIRegistryHost:
|
||||
return p.hub, registry.DHIRegistryHost
|
||||
case registry.StagingRegistryHost:
|
||||
return p.staging, registry.StagingRegistryHost
|
||||
default:
|
||||
return nil, ""
|
||||
}
|
||||
@@ -94,29 +95,35 @@ func (p *provider) sessionFor(registryHostname string) (*session, string) {
|
||||
|
||||
type session struct {
|
||||
name string
|
||||
connect func() (hubSessions, error)
|
||||
connect func() (HubClient, error)
|
||||
timeout time.Duration
|
||||
now func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
hub hubSessions
|
||||
hub HubClient
|
||||
retryAt time.Time
|
||||
cached *types.AuthConfig
|
||||
validUntil time.Time
|
||||
}
|
||||
|
||||
func newSession(name string, opts ...dockerhub.Option) *session {
|
||||
func newSession(name string, hub HubClient) *session {
|
||||
return &session{
|
||||
name: name,
|
||||
connect: func() (hubSessions, error) {
|
||||
return connect(api.DesktopSocketPath(), opts...)
|
||||
},
|
||||
name: name,
|
||||
hub: hub,
|
||||
timeout: lookupTimeout,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func connect(socketPath string, opts ...dockerhub.Option) (hubSessions, error) {
|
||||
func newDesktopSession(name string, opts ...dockerhub.Option) *session {
|
||||
s := newSession(name, nil)
|
||||
s.connect = func() (HubClient, error) {
|
||||
return connect(api.DesktopSocketPath(), opts...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func connect(socketPath string, opts ...dockerhub.Option) (HubClient, error) {
|
||||
if _, err := os.Lstat(socketPath); errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -64,47 +65,47 @@ func hubSession(username string, expiresAt time.Time) dockerhub.UserSession {
|
||||
return s
|
||||
}
|
||||
|
||||
type recordingFallback struct {
|
||||
mu sync.Mutex
|
||||
hosts []string
|
||||
}
|
||||
|
||||
func (f *recordingFallback) GetAuthConfig(registryHostname string) (types.AuthConfig, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.hosts = append(f.hosts, registryHostname)
|
||||
return cliCredentials(registryHostname), nil
|
||||
}
|
||||
|
||||
func cliCredentials(registryHostname string) types.AuthConfig {
|
||||
return types.AuthConfig{Username: "cli-user", Password: "cli-password", ServerAddress: registryHostname}
|
||||
}
|
||||
|
||||
func desktopCredentials() types.AuthConfig {
|
||||
return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: indexServer}
|
||||
return types.AuthConfig{Username: "hubuser", Password: "desktop-oauth-token", ServerAddress: registry.IndexServer}
|
||||
}
|
||||
|
||||
func stagingCredentials() types.AuthConfig {
|
||||
return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: stagingRegistryHost}
|
||||
return types.AuthConfig{Username: "stageuser", Password: "staging-oauth-token", ServerAddress: registry.StagingRegistryHost}
|
||||
}
|
||||
|
||||
type testProvider struct {
|
||||
*provider
|
||||
*Provider
|
||||
fallback *recordingFallback
|
||||
clock *fakeClock
|
||||
connects atomic.Int32
|
||||
staging *fakeHub
|
||||
}
|
||||
|
||||
func newTestProvider(hub hubSessions, connectErr error) *testProvider {
|
||||
func newTestProvider(hub HubClient) *testProvider {
|
||||
tp := &testProvider{
|
||||
clock: &fakeClock{t: testNow},
|
||||
staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile},
|
||||
fallback: &recordingFallback{},
|
||||
clock: &fakeClock{t: testNow},
|
||||
staging: &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile},
|
||||
}
|
||||
tp.provider = &provider{
|
||||
hub: &session{
|
||||
name: "Docker Hub",
|
||||
connect: func() (hubSessions, error) {
|
||||
tp.connects.Add(1)
|
||||
if connectErr != nil {
|
||||
return nil, connectErr
|
||||
}
|
||||
return hub, nil
|
||||
},
|
||||
timeout: time.Second,
|
||||
now: tp.clock.now,
|
||||
},
|
||||
staging: &session{
|
||||
name: "Docker Hub staging",
|
||||
connect: func() (hubSessions, error) {
|
||||
return tp.staging, nil
|
||||
},
|
||||
timeout: time.Second,
|
||||
now: tp.clock.now,
|
||||
},
|
||||
tp.Provider = New(tp.fallback, hub, tp.staging)
|
||||
for _, s := range []*session{tp.Provider.hub, tp.Provider.staging} {
|
||||
s.timeout = time.Second
|
||||
s.now = tp.clock.now
|
||||
}
|
||||
return tp
|
||||
}
|
||||
@@ -123,75 +124,68 @@ func connectError(reason seclient.ConnectReason) error {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableSkipsDockerDesktop(t *testing.T) {
|
||||
Disable()
|
||||
|
||||
_, ok := AuthConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
|
||||
shared.hub.mu.Lock()
|
||||
defer shared.hub.mu.Unlock()
|
||||
assert.Assert(t, shared.hub.retryAt.IsZero(), "Docker Desktop must not be contacted once disabled")
|
||||
assert.Assert(t, shared.hub.hub == nil)
|
||||
}
|
||||
|
||||
func TestSharedProviderHasOneSessionPerEnvironment(t *testing.T) {
|
||||
assert.Assert(t, shared.hub != nil)
|
||||
assert.Assert(t, shared.staging != nil)
|
||||
assert.Assert(t, shared.hub != shared.staging, "production and staging sessions must be separate")
|
||||
func TestNewDesktopSharesOneSessionPerEnvironment(t *testing.T) {
|
||||
a := NewDesktop(&recordingFallback{})
|
||||
b := NewDesktop(&recordingFallback{})
|
||||
assert.Assert(t, a.hub == b.hub, "providers must share the process-wide Docker Hub session")
|
||||
assert.Assert(t, a.staging == b.staging, "providers must share the process-wide Docker Hub staging session")
|
||||
assert.Assert(t, a.hub != a.staging, "production and staging sessions must be separate")
|
||||
}
|
||||
|
||||
func TestDockerHubUsesDesktopSession(t *testing.T) {
|
||||
for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", indexServer} {
|
||||
for _, host := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", registry.IndexServer} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
got, ok := p.authConfig(host)
|
||||
assert.Assert(t, ok)
|
||||
got, err := p.GetAuthConfig(host)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, got.IdentityToken, "", "an IdentityToken is sent as a refresh token")
|
||||
assert.Equal(t, len(p.fallback.hosts), 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOtherRegistriesDoNotContactDesktop(t *testing.T) {
|
||||
func TestOtherRegistriesUseFallback(t *testing.T) {
|
||||
hosts := []string{
|
||||
"ghcr.io", "localhost:5000", "registry.example.com", "",
|
||||
stagingRegistryHost + ".example.com", "example.com/" + stagingRegistryHost,
|
||||
dhiRegistryHost + ".example.com", "example.com/" + dhiRegistryHost, "registry." + dhiRegistryHost,
|
||||
registry.StagingRegistryHost + ".example.com", "example.com/" + registry.StagingRegistryHost,
|
||||
registry.DHIRegistryHost + ".example.com", "example.com/" + registry.DHIRegistryHost, "registry." + registry.DHIRegistryHost,
|
||||
"docker.io.example.com",
|
||||
}
|
||||
for _, host := range hosts {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
p.signInToStaging()
|
||||
|
||||
_, ok := p.authConfig(host)
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, p.connects.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
|
||||
got, err := p.GetAuthConfig(host)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials(host))
|
||||
assert.DeepEqual(t, p.fallback.hosts, []string{host})
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
|
||||
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "Docker Desktop must only be contacted for Docker Hub")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDHIUsesDockerHubSession(t *testing.T) {
|
||||
for _, host := range []string{dhiRegistryHost, "https://" + dhiRegistryHost, "https://" + dhiRegistryHost + "/"} {
|
||||
for _, host := range []string{registry.DHIRegistryHost, "https://" + registry.DHIRegistryHost, "https://" + registry.DHIRegistryHost + "/"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
p.signInToStaging()
|
||||
|
||||
want := desktopCredentials()
|
||||
want.ServerAddress = dhiRegistryHost
|
||||
got, ok := p.authConfig(host)
|
||||
assert.Assert(t, ok)
|
||||
want.ServerAddress = registry.DHIRegistryHost
|
||||
got, err := p.GetAuthConfig(host)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, want)
|
||||
assert.Equal(t, p.staging.sessionCalls.Load(), int32(0), "the staging session must not be read for DHI")
|
||||
|
||||
got, ok = p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
||||
})
|
||||
@@ -199,14 +193,14 @@ func TestDHIUsesDockerHubSession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStagingRegistryUsesStagingSession(t *testing.T) {
|
||||
for _, host := range []string{stagingRegistryHost, "https://" + stagingRegistryHost + "/"} {
|
||||
for _, host := range []string{registry.StagingRegistryHost, "https://" + registry.StagingRegistryHost + "/"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
p.signInToStaging()
|
||||
|
||||
got, ok := p.authConfig(host)
|
||||
assert.Assert(t, ok)
|
||||
got, err := p.GetAuthConfig(host)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, stagingCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(0), "the production session must not be read for the staging registry")
|
||||
})
|
||||
@@ -215,25 +209,27 @@ func TestStagingRegistryUsesStagingSession(t *testing.T) {
|
||||
|
||||
func TestEnvironmentsDoNotMix(t *testing.T) {
|
||||
t.Run("staging session is not used for docker.io", func(t *testing.T) {
|
||||
p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}, nil)
|
||||
p := newTestProvider(&fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile})
|
||||
p.signInToStaging()
|
||||
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
||||
|
||||
got, ok := p.authConfig(stagingRegistryHost)
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig(registry.StagingRegistryHost)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, stagingCredentials())
|
||||
})
|
||||
|
||||
t.Run("production session is not used for the staging registry", func(t *testing.T) {
|
||||
p := newTestProvider(&fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}, nil)
|
||||
p := newTestProvider(&fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))})
|
||||
|
||||
_, ok := p.authConfig(stagingRegistryHost)
|
||||
assert.Assert(t, !ok)
|
||||
got, err := p.GetAuthConfig(registry.StagingRegistryHost)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials(registry.StagingRegistryHost))
|
||||
|
||||
got, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
})
|
||||
}
|
||||
@@ -243,10 +239,10 @@ func TestUsernameFromProfile(t *testing.T) {
|
||||
session: hubSession("", testNow.Add(time.Hour)),
|
||||
profile: dockerhub.Profile{Username: "hubuser"},
|
||||
}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
got, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
}
|
||||
|
||||
@@ -267,45 +263,55 @@ func TestFallsBackWithoutUsableSession(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := newTestProvider(tc.hub, nil)
|
||||
p := newTestProvider(tc.hub)
|
||||
|
||||
for range 2 {
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
||||
}
|
||||
assert.Equal(t, tc.hub.sessionCalls.Load(), int32(1), "Desktop must not be asked again within retryInterval")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallsBackWhenClientCannotBeCreated(t *testing.T) {
|
||||
p := newTestProvider(nil, errors.New("no socket path"))
|
||||
func TestDesktopFallsBackWhenClientCannotBeCreated(t *testing.T) {
|
||||
var connects atomic.Int32
|
||||
s := newSession("Docker Hub", nil)
|
||||
s.connect = func() (HubClient, error) {
|
||||
connects.Add(1)
|
||||
return nil, errors.New("no socket path")
|
||||
}
|
||||
p := &Provider{fallback: &recordingFallback{}, hub: s}
|
||||
|
||||
for range 2 {
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
||||
}
|
||||
assert.Equal(t, p.connects.Load(), int32(1))
|
||||
assert.Equal(t, connects.Load(), int32(1))
|
||||
}
|
||||
|
||||
func TestRetriesAfterInterval(t *testing.T) {
|
||||
hub := &fakeHub{sessionErr: dockerhub.ErrNoDefaultProfile}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
||||
|
||||
hub.sessionErr = nil
|
||||
hub.session = hubSession("hubuser", testNow.Add(time.Hour))
|
||||
|
||||
p.clock.advance(retryInterval - time.Second)
|
||||
_, ok = p.authConfig("docker.io")
|
||||
assert.Assert(t, !ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, cliCredentials("docker.io"))
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
||||
|
||||
p.clock.advance(time.Second)
|
||||
got, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
||||
}
|
||||
@@ -313,53 +319,58 @@ func TestRetriesAfterInterval(t *testing.T) {
|
||||
func TestReusesSessionUntilExpiry(t *testing.T) {
|
||||
expiresAt := testNow.Add(10 * time.Minute)
|
||||
hub := &fakeHub{session: hubSession("hubuser", expiresAt)}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
p.clock.advance(5 * time.Minute)
|
||||
_, ok = p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
||||
|
||||
hub.session = hubSession("hubuser", expiresAt.Add(time.Hour))
|
||||
hub.session.AccessToken = "refreshed-token"
|
||||
p.clock.advance(5*time.Minute - sessionExpirySkew)
|
||||
|
||||
got, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, got.Password, "refreshed-token")
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
||||
}
|
||||
|
||||
func TestRefetchesSessionWithoutExpiry(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", time.Time{})}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
_, ok := p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err := p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
p.clock.advance(sessionTTL - time.Second)
|
||||
_, ok = p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
||||
|
||||
p.clock.advance(time.Second)
|
||||
_, ok = p.authConfig("docker.io")
|
||||
assert.Assert(t, ok)
|
||||
got, err = p.GetAuthConfig("docker.io")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, got, desktopCredentials())
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(2))
|
||||
}
|
||||
|
||||
func TestConcurrentLookupsShareOneFetch(t *testing.T) {
|
||||
hub := &fakeHub{session: hubSession("hubuser", testNow.Add(time.Hour))}
|
||||
p := newTestProvider(hub, nil)
|
||||
p := newTestProvider(hub)
|
||||
|
||||
const lookups = 20
|
||||
results := make([]types.AuthConfig, lookups)
|
||||
var wg sync.WaitGroup
|
||||
for i := range lookups {
|
||||
wg.Go(func() {
|
||||
auth, ok := p.authConfig("docker.io")
|
||||
assert.Check(t, ok)
|
||||
auth, err := p.GetAuthConfig("docker.io")
|
||||
assert.Check(t, err)
|
||||
results[i] = auth
|
||||
})
|
||||
}
|
||||
@@ -369,7 +380,6 @@ func TestConcurrentLookupsShareOneFetch(t *testing.T) {
|
||||
assert.DeepEqual(t, auth, desktopCredentials())
|
||||
}
|
||||
assert.Equal(t, hub.sessionCalls.Load(), int32(1))
|
||||
assert.Equal(t, p.connects.Load(), int32(1))
|
||||
}
|
||||
|
||||
func TestConnectSkipsMissingSocket(t *testing.T) {
|
||||
|
||||
+16
-1
@@ -2,6 +2,7 @@ package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/registryclient"
|
||||
"github.com/docker/cli/internal/secretsengine"
|
||||
"github.com/docker/secrets-engine/client/dockerhub"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
@@ -36,7 +38,6 @@ type FakeCli struct {
|
||||
|
||||
// NewFakeCli returns a fake for the command.Cli interface
|
||||
func NewFakeCli(apiClient client.APIClient, opts ...func(*FakeCli)) *FakeCli {
|
||||
secretsengine.Disable()
|
||||
outBuffer := new(bytes.Buffer)
|
||||
errBuffer := new(bytes.Buffer)
|
||||
c := &FakeCli{
|
||||
@@ -178,6 +179,20 @@ func (c *FakeCli) SetRegistryClient(registryClient registryclient.RegistryClient
|
||||
c.registryClient = registryClient
|
||||
}
|
||||
|
||||
type signedOutHub struct{}
|
||||
|
||||
func (signedOutHub) GetDefaultSession(context.Context) (dockerhub.UserSession, error) {
|
||||
return dockerhub.UserSession{}, dockerhub.ErrNoSession
|
||||
}
|
||||
|
||||
func (signedOutHub) GetDefaultProfile(context.Context) (dockerhub.Profile, error) {
|
||||
return dockerhub.Profile{}, dockerhub.ErrNoSession
|
||||
}
|
||||
|
||||
func (c *FakeCli) RegistryAuthProvider() secretsengine.AuthProvider {
|
||||
return secretsengine.New(c.configfile, signedOutHub{}, signedOutHub{})
|
||||
}
|
||||
|
||||
// BuildKitEnabled on the fake cli
|
||||
func (*FakeCli) BuildKitEnabled() (bool, error) {
|
||||
return true, nil
|
||||
|
||||
Reference in New Issue
Block a user