Merge pull request #5667 from thaJeztah/login_clean

cli/command/registry: assorted refactor and test changes
This commit is contained in:
Sebastiaan van Stijn 2024-12-06 14:42:22 +01:00 committed by GitHub
commit 97010d13f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 64 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/config/configfile"
configtypes "github.com/docker/cli/cli/config/types" configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/internal/oauth/manager" "github.com/docker/cli/cli/internal/oauth/manager"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
@ -88,7 +89,7 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
} }
var ( var (
serverAddress string serverAddress string
response *registrytypes.AuthenticateOKBody msg string
) )
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
serverAddress = opts.serverAddress serverAddress = opts.serverAddress
@ -100,25 +101,25 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
// attempt login with current (stored) credentials // attempt login with current (stored) credentials
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry) authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
if err == nil && authConfig.Username != "" && authConfig.Password != "" { if err == nil && authConfig.Username != "" && authConfig.Password != "" {
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig) msg, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
} }
// if we failed to authenticate with stored credentials (or didn't have stored credentials), // if we failed to authenticate with stored credentials (or didn't have stored credentials),
// prompt the user for new credentials // prompt the user for new credentials
if err != nil || authConfig.Username == "" || authConfig.Password == "" { if err != nil || authConfig.Username == "" || authConfig.Password == "" {
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, authConfig.ServerAddress) msg, err = loginUser(ctx, dockerCli, opts, authConfig.Username, authConfig.ServerAddress)
if err != nil { if err != nil {
return err return err
} }
} }
if response != nil && response.Status != "" { if msg != "" {
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status) _, _ = fmt.Fprintln(dockerCli.Out(), msg)
} }
return nil return nil
} }
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) { func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (msg string, _ error) {
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n") _, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig) response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
if err != nil { if err != nil {
@ -134,11 +135,11 @@ func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, auth
authConfig.IdentityToken = response.IdentityToken authConfig.IdentityToken = response.IdentityToken
} }
if err := storeCredentials(dockerCli, authConfig); err != nil { if err := storeCredentials(dockerCli.ConfigFile(), authConfig); err != nil {
return nil, err return "", err
} }
return &response, err return response.Status, err
} }
const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN" const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN"
@ -154,7 +155,7 @@ func isOauthLoginDisabled() bool {
return false return false
} }
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) { func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
// Some links documenting this: // Some links documenting this:
// - https://code.google.com/archive/p/mintty/issues/56 // - https://code.google.com/archive/p/mintty/issues/56
// - https://github.com/docker/docker/issues/15272 // - https://github.com/docker/docker/issues/15272
@ -163,16 +164,17 @@ func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, de
// will hit this if you attempt docker login from mintty where stdin // will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console. // is a pipe, not a character based console.
if (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() { if (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() {
return nil, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") return "", errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
} }
// If we're logging into the index server and the user didn't provide a username or password, use the device flow // If we're logging into the index server and the user didn't provide a username or password, use the device flow
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() { if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
response, err := loginWithDeviceCodeFlow(ctx, dockerCli) var err error
msg, err = loginWithDeviceCodeFlow(ctx, dockerCli)
// if the error represents a failure to initiate the device-code flow, // if the error represents a failure to initiate the device-code flow,
// then we fallback to regular cli credentials login // then we fallback to regular cli credentials login
if !errors.Is(err, manager.ErrDeviceLoginStartFail) { if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return response, err return msg, err
} }
_, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n") _, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
} }
@ -180,50 +182,50 @@ func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, de
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress) return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
} }
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) { func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) {
// Prompt user for credentials // Prompt user for credentials
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress) authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
if err != nil { if err != nil {
return nil, err return "", err
} }
response, err := loginWithRegistry(ctx, dockerCli, authConfig) response, err := loginWithRegistry(ctx, dockerCli.Client(), authConfig)
if err != nil { if err != nil {
return nil, err return "", err
} }
if response.IdentityToken != "" { if response.IdentityToken != "" {
authConfig.Password = "" authConfig.Password = ""
authConfig.IdentityToken = response.IdentityToken authConfig.IdentityToken = response.IdentityToken
} }
if err = storeCredentials(dockerCli, authConfig); err != nil { if err = storeCredentials(dockerCli.ConfigFile(), authConfig); err != nil {
return nil, err return "", err
} }
return &response, nil return response.Status, nil
} }
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) { func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (msg string, _ error) {
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer) store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err()) authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
if err != nil { if err != nil {
return nil, err return "", err
} }
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig)) response, err := loginWithRegistry(ctx, dockerCli.Client(), registrytypes.AuthConfig(*authConfig))
if err != nil { if err != nil {
return nil, err return "", err
} }
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil { if err = storeCredentials(dockerCli.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil {
return nil, err return "", err
} }
return &response, nil return response.Status, nil
} }
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error { func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error {
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress) creds := cfg.GetCredentialsStore(authConfig.ServerAddress)
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil { if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
return errors.Errorf("Error saving credentials: %v", err) return errors.Errorf("Error saving credentials: %v", err)
} }
@ -231,30 +233,32 @@ func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig
return nil return nil
} }
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { func loginWithRegistry(ctx context.Context, apiClient client.SystemAPIClient, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig) response, err := apiClient.RegistryLogin(ctx, authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, authConfig)
}
// If we (still) have an error, give up
if err != nil { if err != nil {
return registrytypes.AuthenticateOKBody{}, err if client.IsErrConnectionFailed(err) {
// daemon isn't responding; attempt to login client side.
return loginClientSide(ctx, authConfig)
}
return nil, err
} }
return response, nil return &response, nil
} }
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
svc, err := registry.NewService(registry.ServiceOptions{}) svc, err := registry.NewService(registry.ServiceOptions{})
if err != nil { if err != nil {
return registrytypes.AuthenticateOKBody{}, err return nil, err
} }
status, token, err := svc.Auth(ctx, &auth, command.UserAgent()) status, token, err := svc.Auth(ctx, &auth, command.UserAgent())
if err != nil {
return nil, err
}
return registrytypes.AuthenticateOKBody{ return &registrytypes.AuthenticateOKBody{
Status: status, Status: status,
IdentityToken: token, IdentityToken: token,
}, err }, nil
} }

View File

@ -1,10 +1,10 @@
package registry package registry
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"path/filepath"
"testing" "testing"
"time" "time"
@ -55,8 +55,9 @@ func (c *fakeClient) RegistryLogin(_ context.Context, auth registrytypes.AuthCon
func TestLoginWithCredStoreCreds(t *testing.T) { func TestLoginWithCredStoreCreds(t *testing.T) {
testCases := []struct { testCases := []struct {
inputAuthConfig registrytypes.AuthConfig inputAuthConfig registrytypes.AuthConfig
expectedMsg string
expectedErr string expectedErr string
expectedMsg string
expectedErrMsg string
}{ }{
{ {
inputAuthConfig: registrytypes.AuthConfig{}, inputAuthConfig: registrytypes.AuthConfig{},
@ -66,20 +67,25 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
inputAuthConfig: registrytypes.AuthConfig{ inputAuthConfig: registrytypes.AuthConfig{
Username: unknownUser, Username: unknownUser,
}, },
expectedMsg: "Authenticating with existing credentials...\n", expectedErr: errUnknownUser,
expectedErr: fmt.Sprintf("Login did not succeed, error: %s\n", errUnknownUser), expectedMsg: "Authenticating with existing credentials...\n",
expectedErrMsg: fmt.Sprintf("Login did not succeed, error: %s\n", errUnknownUser),
}, },
} }
ctx := context.Background() ctx := context.Background()
cli := test.NewFakeCli(&fakeClient{})
cli.ConfigFile().Filename = filepath.Join(t.TempDir(), "config.json")
for _, tc := range testCases { for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{}) _, err := loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
errBuf := new(bytes.Buffer) if tc.expectedErrMsg != "" {
cli.SetErr(streams.NewOut(errBuf)) assert.Check(t, is.Error(err, tc.expectedErr))
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig) } else {
outputString := cli.OutBuffer().String() assert.NilError(t, err)
assert.Check(t, is.Equal(tc.expectedMsg, outputString)) }
errorString := errBuf.String() assert.Check(t, is.Equal(tc.expectedMsg, cli.OutBuffer().String()))
assert.Check(t, is.Equal(tc.expectedErr, errorString)) assert.Check(t, is.Equal(tc.expectedErrMsg, cli.ErrBuffer().String()))
cli.ErrBuffer().Reset()
cli.OutBuffer().Reset()
} }
} }
@ -349,16 +355,16 @@ func TestLoginNonInteractive(t *testing.T) {
// "" meaning default registry // "" meaning default registry
registries := []string{"", "my-registry.com"} registries := []string{"", "my-registry.com"}
for _, registry := range registries { for _, registryAddr := range registries {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
tmpFile := fs.NewFile(t, "test-run-login") tmpFile := fs.NewFile(t, "test-run-login")
defer tmpFile.Remove() defer tmpFile.Remove()
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
configfile := cli.ConfigFile() cfg := cli.ConfigFile()
configfile.Filename = tmpFile.Path() cfg.Filename = tmpFile.Path()
options := loginOptions{ options := loginOptions{
serverAddress: registry, serverAddress: registryAddr,
} }
if tc.username { if tc.username {
options.user = "my-username" options.user = "my-username"
@ -412,26 +418,26 @@ func TestLoginNonInteractive(t *testing.T) {
// "" meaning default registry // "" meaning default registry
registries := []string{"", "my-registry.com"} registries := []string{"", "my-registry.com"}
for _, registry := range registries { for _, registryAddr := range registries {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) { t.Run(tc.doc, func(t *testing.T) {
tmpFile := fs.NewFile(t, "test-run-login") tmpFile := fs.NewFile(t, "test-run-login")
defer tmpFile.Remove() defer tmpFile.Remove()
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
configfile := cli.ConfigFile() cfg := cli.ConfigFile()
configfile.Filename = tmpFile.Path() cfg.Filename = tmpFile.Path()
serverAddress := registry serverAddress := registryAddr
if serverAddress == "" { if serverAddress == "" {
serverAddress = "https://index.docker.io/v1/" serverAddress = "https://index.docker.io/v1/"
} }
assert.NilError(t, configfile.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{ assert.NilError(t, cfg.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{
Username: "my-username", Username: "my-username",
Password: "my-password", Password: "my-password",
ServerAddress: serverAddress, ServerAddress: serverAddress,
})) }))
options := loginOptions{ options := loginOptions{
serverAddress: registry, serverAddress: registryAddr,
} }
if tc.username { if tc.username {
options.user = "my-username" options.user = "my-username"