Skip to content

Commit 6e4818e

Browse files
committed
Refactor cli/command/registry
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
1 parent fcfdd7b commit 6e4818e

File tree

3 files changed

+148
-67
lines changed

3 files changed

+148
-67
lines changed

cli/command/registry.go

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
4141
default:
4242
}
4343

44-
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
44+
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
4545
if err != nil {
4646
return "", err
4747
}
@@ -86,8 +86,32 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
8686
return registrytypes.AuthConfig(authconfig), nil
8787
}
8888

89-
// ConfigureAuth handles prompting of user's username and password if needed
90-
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
89+
// ConfigureAuth handles prompting of user's username and password if needed.
90+
// Deprecated: use PromptUserForCredentials instead.
91+
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
92+
defaultUsername := authConfig.Username
93+
serverAddress := authConfig.ServerAddress
94+
95+
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress)
96+
if err != nil {
97+
return err
98+
}
99+
100+
authConfig.Username = newAuthConfig.Username
101+
authConfig.Password = newAuthConfig.Password
102+
return nil
103+
}
104+
105+
// PromptUserForCredentials handles the CLI prompt for the user to input
106+
// credentials.
107+
// If argUser is not empty, then the user is only prompted for their password.
108+
// If argPassword is not empty, then the user is only prompted for their username
109+
// If neither argUser nor argPassword are empty, then the user is not prompted and
110+
// an AuthConfig is returned with those values.
111+
// If defaultUsername is not empty, the username prompt includes that username
112+
// and the user can hit enter without inputting a username to use that default
113+
// username.
114+
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
91115
// On Windows, force the use of the regular OS stdin stream.
92116
//
93117
// See:
@@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
107131
// Linux will hit this if you attempt `cat | docker login`, and Windows
108132
// will hit this if you attempt docker login from mintty where stdin
109133
// is a pipe, not a character based console.
110-
if flPassword == "" && !cli.In().IsTerminal() {
111-
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
134+
if argPassword == "" && !cli.In().IsTerminal() {
135+
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
112136
}
113137

114-
authconfig.Username = strings.TrimSpace(authconfig.Username)
138+
isDefaultRegistry := serverAddress == registry.IndexServer
139+
defaultUsername = strings.TrimSpace(defaultUsername)
115140

116-
if flUser = strings.TrimSpace(flUser); flUser == "" {
141+
if argUser = strings.TrimSpace(argUser); argUser == "" {
117142
if isDefaultRegistry {
118143
// if this is a default registry (docker hub), then display the following message.
119144
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
@@ -124,44 +149,43 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
124149
}
125150

126151
var prompt string
127-
if authconfig.Username == "" {
152+
if defaultUsername == "" {
128153
prompt = "Username: "
129154
} else {
130-
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
155+
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
131156
}
132-
var err error
133-
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
157+
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
134158
if err != nil {
135-
return err
159+
return authConfig, err
136160
}
137-
if flUser == "" {
138-
flUser = authconfig.Username
161+
if argUser == "" {
162+
argUser = defaultUsername
139163
}
140164
}
141-
if flUser == "" {
142-
return errors.Errorf("Error: Non-null Username Required")
165+
if argUser == "" {
166+
return authConfig, errors.Errorf("Error: Non-null Username Required")
143167
}
144-
if flPassword == "" {
168+
if argPassword == "" {
145169
restoreInput, err := DisableInputEcho(cli.In())
146170
if err != nil {
147-
return err
171+
return authConfig, err
148172
}
149173
defer restoreInput()
150174

151-
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
175+
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
152176
if err != nil {
153-
return err
177+
return authConfig, err
154178
}
155179
fmt.Fprint(cli.Out(), "\n")
156-
if flPassword == "" {
157-
return errors.Errorf("Error: Password Required")
180+
if argPassword == "" {
181+
return authConfig, errors.Errorf("Error: Password Required")
158182
}
159183
}
160184

161-
authconfig.Username = flUser
162-
authconfig.Password = flPassword
163-
164-
return nil
185+
authConfig.Username = argUser
186+
authConfig.Password = argPassword
187+
authConfig.ServerAddress = serverAddress
188+
return authConfig, nil
165189
}
166190

167191
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

cli/command/registry/login.go

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -80,80 +80,137 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
8080
return nil
8181
}
8282

83-
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo
84-
clnt := dockerCli.Client()
83+
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
8584
if err := verifyloginOptions(dockerCli, &opts); err != nil {
8685
return err
8786
}
8887
var (
8988
serverAddress string
90-
response registrytypes.AuthenticateOKBody
89+
response *registrytypes.AuthenticateOKBody
9190
)
9291
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
9392
serverAddress = opts.serverAddress
9493
} else {
9594
serverAddress = registry.IndexServer
9695
}
97-
9896
isDefaultRegistry := serverAddress == registry.IndexServer
97+
98+
// attempt login with current (stored) credentials
9999
authConfig, err := command.GetDefaultAuthConfig(dockerCli.ConfigFile(), opts.user == "" && opts.password == "", serverAddress, isDefaultRegistry)
100100
if err == nil && authConfig.Username != "" && authConfig.Password != "" {
101-
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
101+
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
102102
}
103-
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
104-
if isDefaultRegistry && opts.user == "" && opts.password == "" {
105-
// todo(laurazard: clean this up
106-
store := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
107-
oauthAuthConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
108-
if err != nil {
109-
return err
110-
}
111-
authConfig = registrytypes.AuthConfig(*oauthAuthConfig)
112-
} else {
113-
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
114-
if err != nil {
115-
return err
116-
}
117-
}
118103

119-
response, err = clnt.RegistryLogin(ctx, authConfig)
120-
if err != nil && client.IsErrConnectionFailed(err) {
121-
// If the server isn't responding (yet) attempt to login purely client side
122-
response, err = loginClientSide(ctx, authConfig)
123-
}
124-
// If we (still) have an error, give up
104+
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
105+
// prompt the user for new credentials
106+
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
107+
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
125108
if err != nil {
126109
return err
127110
}
128111
}
112+
113+
if response != nil && response.Status != "" {
114+
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
115+
}
116+
return nil
117+
}
118+
119+
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
120+
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
121+
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
122+
if err != nil {
123+
if errdefs.IsUnauthorized(err) {
124+
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
125+
} else {
126+
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
127+
}
128+
}
129+
129130
if response.IdentityToken != "" {
130131
authConfig.Password = ""
131132
authConfig.IdentityToken = response.IdentityToken
132133
}
133134

134-
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
135+
if err := storeCredentials(dockerCli, authConfig); err != nil {
136+
return nil, err
137+
}
138+
139+
return &response, err
140+
}
141+
142+
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
143+
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
144+
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" {
145+
return loginWithDeviceCodeFlow(ctx, dockerCli)
146+
} else {
147+
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
148+
}
149+
}
150+
151+
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
152+
// Prompt user for credentials
153+
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
if response.IdentityToken != "" {
164+
authConfig.Password = ""
165+
authConfig.IdentityToken = response.IdentityToken
166+
}
167+
if err = storeCredentials(dockerCli, authConfig); err != nil {
168+
return nil, err
169+
}
170+
171+
return &response, nil
172+
}
173+
174+
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
175+
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
176+
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
182+
if err != nil {
183+
return nil, err
184+
}
185+
186+
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
187+
return nil, err
188+
}
189+
190+
return &response, nil
191+
}
192+
193+
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
194+
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
135195
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
136196
return errors.Errorf("Error saving credentials: %v", err)
137197
}
138198

139-
if response.Status != "" {
140-
fmt.Fprintln(dockerCli.Out(), response.Status)
141-
}
142199
return nil
143200
}
144201

145-
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
146-
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
147-
cliClient := dockerCli.Client()
148-
response, err := cliClient.RegistryLogin(ctx, *authConfig)
202+
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
203+
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
204+
if err != nil && client.IsErrConnectionFailed(err) {
205+
// If the server isn't responding (yet) attempt to login purely client side
206+
response, err = loginClientSide(ctx, authConfig)
207+
}
208+
// If we (still) have an error, give up
149209
if err != nil {
150-
if errdefs.IsUnauthorized(err) {
151-
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
152-
} else {
153-
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
154-
}
210+
return registrytypes.AuthenticateOKBody{}, err
155211
}
156-
return response, err
212+
213+
return response, nil
157214
}
158215

159216
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {

cli/command/registry/login_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
7474
cli := test.NewFakeCli(&fakeClient{})
7575
errBuf := new(bytes.Buffer)
7676
cli.SetErr(streams.NewOut(errBuf))
77-
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
77+
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
7878
outputString := cli.OutBuffer().String()
7979
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
8080
errorString := errBuf.String()

0 commit comments

Comments
 (0)