mirror of https://github.com/docker/cli.git
Refactor `cli/command/registry`
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 6e4818e7d6
)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
This commit is contained in:
parent
0c29d6bac1
commit
b8a38fd22d
|
@ -41,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
|
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -86,8 +86,32 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
|
||||||
return registrytypes.AuthConfig(authconfig), nil
|
return registrytypes.AuthConfig(authconfig), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureAuth handles prompting of user's username and password if needed
|
// ConfigureAuth handles prompting of user's username and password if needed.
|
||||||
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
|
// Deprecated: use PromptUserForCredentials instead.
|
||||||
|
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
|
||||||
|
defaultUsername := authConfig.Username
|
||||||
|
serverAddress := authConfig.ServerAddress
|
||||||
|
|
||||||
|
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig.Username = newAuthConfig.Username
|
||||||
|
authConfig.Password = newAuthConfig.Password
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptUserForCredentials handles the CLI prompt for the user to input
|
||||||
|
// credentials.
|
||||||
|
// If argUser is not empty, then the user is only prompted for their password.
|
||||||
|
// If argPassword is not empty, then the user is only prompted for their username
|
||||||
|
// If neither argUser nor argPassword are empty, then the user is not prompted and
|
||||||
|
// an AuthConfig is returned with those values.
|
||||||
|
// If defaultUsername is not empty, the username prompt includes that username
|
||||||
|
// and the user can hit enter without inputting a username to use that default
|
||||||
|
// username.
|
||||||
|
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
|
||||||
// On Windows, force the use of the regular OS stdin stream.
|
// On Windows, force the use of the regular OS stdin stream.
|
||||||
//
|
//
|
||||||
// See:
|
// See:
|
||||||
|
@ -107,13 +131,14 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
|
||||||
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||||
// 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 flPassword == "" && !cli.In().IsTerminal() {
|
if argPassword == "" && !cli.In().IsTerminal() {
|
||||||
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||||
}
|
}
|
||||||
|
|
||||||
authconfig.Username = strings.TrimSpace(authconfig.Username)
|
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||||
|
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||||
|
|
||||||
if flUser = strings.TrimSpace(flUser); flUser == "" {
|
if argUser = strings.TrimSpace(argUser); argUser == "" {
|
||||||
if isDefaultRegistry {
|
if isDefaultRegistry {
|
||||||
// if this is a default registry (docker hub), then display the following message.
|
// if this is a default registry (docker hub), then display the following message.
|
||||||
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.")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var prompt string
|
var prompt string
|
||||||
if authconfig.Username == "" {
|
if defaultUsername == "" {
|
||||||
prompt = "Username: "
|
prompt = "Username: "
|
||||||
} else {
|
} else {
|
||||||
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
|
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
|
||||||
}
|
}
|
||||||
var err error
|
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||||
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return authConfig, err
|
||||||
}
|
}
|
||||||
if flUser == "" {
|
if argUser == "" {
|
||||||
flUser = authconfig.Username
|
argUser = defaultUsername
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if flUser == "" {
|
if argUser == "" {
|
||||||
return errors.Errorf("Error: Non-null Username Required")
|
return authConfig, errors.Errorf("Error: Non-null Username Required")
|
||||||
}
|
}
|
||||||
if flPassword == "" {
|
if argPassword == "" {
|
||||||
restoreInput, err := DisableInputEcho(cli.In())
|
restoreInput, err := DisableInputEcho(cli.In())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return authConfig, err
|
||||||
}
|
}
|
||||||
defer restoreInput()
|
defer restoreInput()
|
||||||
|
|
||||||
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return authConfig, err
|
||||||
}
|
}
|
||||||
fmt.Fprint(cli.Out(), "\n")
|
fmt.Fprint(cli.Out(), "\n")
|
||||||
if flPassword == "" {
|
if argPassword == "" {
|
||||||
return errors.Errorf("Error: Password Required")
|
return authConfig, errors.Errorf("Error: Password Required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authconfig.Username = flUser
|
authConfig.Username = argUser
|
||||||
authconfig.Password = flPassword
|
authConfig.Password = argPassword
|
||||||
|
authConfig.ServerAddress = serverAddress
|
||||||
return nil
|
return authConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
||||||
|
|
|
@ -101,90 +101,146 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { //nolint:gocyclo
|
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
|
||||||
clnt := dockerCli.Client()
|
|
||||||
if err := verifyloginOptions(dockerCli, &opts); err != nil {
|
if err := verifyloginOptions(dockerCli, &opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
serverAddress string
|
serverAddress string
|
||||||
response registrytypes.AuthenticateOKBody
|
response *registrytypes.AuthenticateOKBody
|
||||||
)
|
)
|
||||||
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
|
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
|
||||||
serverAddress = opts.serverAddress
|
serverAddress = opts.serverAddress
|
||||||
} else {
|
} else {
|
||||||
serverAddress = registry.IndexServer
|
serverAddress = registry.IndexServer
|
||||||
}
|
}
|
||||||
|
|
||||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||||
|
|
||||||
|
// 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 = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
|
response, err = loginWithStoredCredentials(ctx, dockerCli, authConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
|
||||||
|
// prompt the user for new credentials
|
||||||
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
||||||
if isDefaultRegistry && opts.user == "" && opts.password == "" {
|
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
|
||||||
// todo(laurazard: clean this up
|
|
||||||
store := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
|
|
||||||
oauthAuthConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
authConfig = registrytypes.AuthConfig(*oauthAuthConfig)
|
|
||||||
} else {
|
|
||||||
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err = clnt.RegistryLogin(ctx, authConfig)
|
if response != nil && response.Status != "" {
|
||||||
|
_, _ = fmt.Fprintln(dockerCli.Out(), response.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithStoredCredentials(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) {
|
||||||
|
_, _ = fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
|
||||||
|
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsUnauthorized(err) {
|
||||||
|
_, _ = fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
|
||||||
|
} else {
|
||||||
|
_, _ = fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IdentityToken != "" {
|
||||||
|
authConfig.Password = ""
|
||||||
|
authConfig.IdentityToken = response.IdentityToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := storeCredentials(dockerCli, authConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
||||||
|
// 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 == "" {
|
||||||
|
return loginWithDeviceCodeFlow(ctx, dockerCli)
|
||||||
|
} else {
|
||||||
|
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithUsernameAndPassword(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
||||||
|
// Prompt user for credentials
|
||||||
|
authConfig, err := command.PromptUserForCredentials(ctx, dockerCli, opts.user, opts.password, defaultUsername, serverAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := loginWithRegistry(ctx, dockerCli, authConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IdentityToken != "" {
|
||||||
|
authConfig.Password = ""
|
||||||
|
authConfig.IdentityToken = response.IdentityToken
|
||||||
|
}
|
||||||
|
if err = storeCredentials(dockerCli, authConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithDeviceCodeFlow(ctx context.Context, dockerCli command.Cli) (*registrytypes.AuthenticateOKBody, error) {
|
||||||
|
store := dockerCli.ConfigFile().GetCredentialsStore(registry.IndexServer)
|
||||||
|
authConfig, err := manager.NewManager(store).LoginDevice(ctx, dockerCli.Err())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := loginWithRegistry(ctx, dockerCli, registrytypes.AuthConfig(*authConfig))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = storeCredentials(dockerCli, registrytypes.AuthConfig(*authConfig)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeCredentials(dockerCli command.Cli, authConfig registrytypes.AuthConfig) error {
|
||||||
|
creds := dockerCli.ConfigFile().GetCredentialsStore(authConfig.ServerAddress)
|
||||||
|
|
||||||
|
store, isDefault := creds.(isFileStore)
|
||||||
|
// Display a warning if we're storing the users password (not a token)
|
||||||
|
if isDefault && authConfig.Password != "" {
|
||||||
|
err := displayUnencryptedWarning(dockerCli, store.GetFilename())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
|
||||||
|
return errors.Errorf("Error saving credentials: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithRegistry(ctx context.Context, dockerCli command.Cli, authConfig registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
||||||
|
response, err := dockerCli.Client().RegistryLogin(ctx, authConfig)
|
||||||
if err != nil && client.IsErrConnectionFailed(err) {
|
if err != nil && client.IsErrConnectionFailed(err) {
|
||||||
// If the server isn't responding (yet) attempt to login purely client side
|
// If the server isn't responding (yet) attempt to login purely client side
|
||||||
response, err = loginClientSide(ctx, authConfig)
|
response, err = loginClientSide(ctx, authConfig)
|
||||||
}
|
}
|
||||||
// If we (still) have an error, give up
|
// If we (still) have an error, give up
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return registrytypes.AuthenticateOKBody{}, err
|
||||||
}
|
|
||||||
}
|
|
||||||
if response.IdentityToken != "" {
|
|
||||||
authConfig.Password = ""
|
|
||||||
authConfig.IdentityToken = response.IdentityToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
creds := dockerCli.ConfigFile().GetCredentialsStore(serverAddress)
|
return response, nil
|
||||||
|
|
||||||
store, isDefault := creds.(isFileStore)
|
|
||||||
// Display a warning if we're storing the users password (not a token)
|
|
||||||
if isDefault && authConfig.Password != "" {
|
|
||||||
err = displayUnencryptedWarning(dockerCli, store.GetFilename())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil {
|
|
||||||
return errors.Errorf("Error saving credentials: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.Status != "" {
|
|
||||||
fmt.Fprintln(dockerCli.Out(), response.Status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authConfig *registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Authenticating with existing credentials...\n")
|
|
||||||
cliClient := dockerCli.Client()
|
|
||||||
response, err := cliClient.RegistryLogin(ctx, *authConfig)
|
|
||||||
if err != nil {
|
|
||||||
if errdefs.IsUnauthorized(err) {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Stored credentials invalid or expired\n")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "Login did not succeed, error: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
|
||||||
|
|
|
@ -74,7 +74,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{})
|
cli := test.NewFakeCli(&fakeClient{})
|
||||||
errBuf := new(bytes.Buffer)
|
errBuf := new(bytes.Buffer)
|
||||||
cli.SetErr(streams.NewOut(errBuf))
|
cli.SetErr(streams.NewOut(errBuf))
|
||||||
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
|
loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
|
||||||
outputString := cli.OutBuffer().String()
|
outputString := cli.OutBuffer().String()
|
||||||
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
|
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
|
||||||
errorString := errBuf.String()
|
errorString := errBuf.String()
|
||||||
|
|
Loading…
Reference in New Issue