github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/registry.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"runtime"
    12  	"strings"
    13  
    14  	configtypes "github.com/docker/cli/cli/config/types"
    15  	"github.com/docker/cli/cli/debug"
    16  	"github.com/docker/cli/cli/streams"
    17  	"github.com/docker/distribution/reference"
    18  	"github.com/docker/docker/api/types"
    19  	registrytypes "github.com/docker/docker/api/types/registry"
    20  	"github.com/docker/docker/registry"
    21  	"github.com/moby/term"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  // ElectAuthServer returns the default registry to use (by asking the daemon)
    26  func ElectAuthServer(ctx context.Context, cli Cli) string {
    27  	// The daemon `/info` endpoint informs us of the default registry being
    28  	// used. This is essential in cross-platforms environment, where for
    29  	// example a Linux client might be interacting with a Windows daemon, hence
    30  	// the default registry URL might be Windows specific.
    31  	info, err := cli.Client().Info(ctx)
    32  	if err != nil {
    33  		// Daemon is not responding so use system default.
    34  		if debug.IsEnabled() {
    35  			// Only report the warning if we're in debug mode to prevent nagging during engine initialization workflows
    36  			fmt.Fprintf(cli.Err(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, registry.IndexServer)
    37  		}
    38  		return registry.IndexServer
    39  	}
    40  	if info.IndexServerAddress == "" {
    41  		if debug.IsEnabled() {
    42  			fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", registry.IndexServer)
    43  		}
    44  		return registry.IndexServer
    45  	}
    46  	return info.IndexServerAddress
    47  }
    48  
    49  // EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload
    50  func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
    51  	buf, err := json.Marshal(authConfig)
    52  	if err != nil {
    53  		return "", err
    54  	}
    55  	return base64.URLEncoding.EncodeToString(buf), nil
    56  }
    57  
    58  // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
    59  // for the given command.
    60  func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
    61  	return func() (string, error) {
    62  		fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName)
    63  		indexServer := registry.GetAuthConfigKey(index)
    64  		isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli)
    65  		authConfig, err := GetDefaultAuthConfig(cli, true, indexServer, isDefaultRegistry)
    66  		if err != nil {
    67  			fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
    68  		}
    69  		err = ConfigureAuth(cli, "", "", authConfig, isDefaultRegistry)
    70  		if err != nil {
    71  			return "", err
    72  		}
    73  		return EncodeAuthToBase64(*authConfig)
    74  	}
    75  }
    76  
    77  // ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the
    78  // default index, it uses the default index name for the daemon's platform,
    79  // not the client's platform.
    80  func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig {
    81  	configKey := index.Name
    82  	if index.Official {
    83  		configKey = ElectAuthServer(ctx, cli)
    84  	}
    85  
    86  	a, _ := cli.ConfigFile().GetAuthConfig(configKey)
    87  	return types.AuthConfig(a)
    88  }
    89  
    90  // GetDefaultAuthConfig gets the default auth config given a serverAddress
    91  // If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
    92  func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) {
    93  	if !isDefaultRegistry {
    94  		serverAddress = registry.ConvertToHostname(serverAddress)
    95  	}
    96  	var authconfig = configtypes.AuthConfig{}
    97  	var err error
    98  	if checkCredStore {
    99  		authconfig, err = cli.ConfigFile().GetAuthConfig(serverAddress)
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  	}
   104  	authconfig.ServerAddress = serverAddress
   105  	authconfig.IdentityToken = ""
   106  	res := types.AuthConfig(authconfig)
   107  	return &res, nil
   108  }
   109  
   110  // ConfigureAuth handles prompting of user's username and password if needed
   111  func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *types.AuthConfig, isDefaultRegistry bool) error {
   112  	// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
   113  	if runtime.GOOS == "windows" {
   114  		cli.SetIn(streams.NewIn(os.Stdin))
   115  	}
   116  
   117  	// Some links documenting this:
   118  	// - https://code.google.com/archive/p/mintty/issues/56
   119  	// - https://github.com/docker/docker/issues/15272
   120  	// - https://mintty.github.io/ (compatibility)
   121  	// Linux will hit this if you attempt `cat | docker login`, and Windows
   122  	// will hit this if you attempt docker login from mintty where stdin
   123  	// is a pipe, not a character based console.
   124  	if flPassword == "" && !cli.In().IsTerminal() {
   125  		return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
   126  	}
   127  
   128  	authconfig.Username = strings.TrimSpace(authconfig.Username)
   129  
   130  	if flUser = strings.TrimSpace(flUser); flUser == "" {
   131  		if isDefaultRegistry {
   132  			// if this is a default registry (docker hub), then display the following message.
   133  			fmt.Fprintln(cli.Out(), "Login with your Docker ID 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.")
   134  		}
   135  		promptWithDefault(cli.Out(), "Username", authconfig.Username)
   136  		flUser = readInput(cli.In(), cli.Out())
   137  		flUser = strings.TrimSpace(flUser)
   138  		if flUser == "" {
   139  			flUser = authconfig.Username
   140  		}
   141  	}
   142  	if flUser == "" {
   143  		return errors.Errorf("Error: Non-null Username Required")
   144  	}
   145  	if flPassword == "" {
   146  		oldState, err := term.SaveState(cli.In().FD())
   147  		if err != nil {
   148  			return err
   149  		}
   150  		fmt.Fprintf(cli.Out(), "Password: ")
   151  		term.DisableEcho(cli.In().FD(), oldState)
   152  
   153  		flPassword = readInput(cli.In(), cli.Out())
   154  		fmt.Fprint(cli.Out(), "\n")
   155  
   156  		term.RestoreTerminal(cli.In().FD(), oldState)
   157  		if flPassword == "" {
   158  			return errors.Errorf("Error: Password Required")
   159  		}
   160  	}
   161  
   162  	authconfig.Username = flUser
   163  	authconfig.Password = flPassword
   164  
   165  	return nil
   166  }
   167  
   168  func readInput(in io.Reader, out io.Writer) string {
   169  	reader := bufio.NewReader(in)
   170  	line, _, err := reader.ReadLine()
   171  	if err != nil {
   172  		fmt.Fprintln(out, err.Error())
   173  		os.Exit(1)
   174  	}
   175  	return string(line)
   176  }
   177  
   178  func promptWithDefault(out io.Writer, prompt string, configDefault string) {
   179  	if configDefault == "" {
   180  		fmt.Fprintf(out, "%s: ", prompt)
   181  	} else {
   182  		fmt.Fprintf(out, "%s (%s): ", prompt, configDefault)
   183  	}
   184  }
   185  
   186  // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
   187  func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) {
   188  	// Retrieve encoded auth token from the image reference
   189  	authConfig, err := resolveAuthConfigFromImage(ctx, cli, image)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	encodedAuth, err := EncodeAuthToBase64(authConfig)
   194  	if err != nil {
   195  		return "", err
   196  	}
   197  	return encodedAuth, nil
   198  }
   199  
   200  // resolveAuthConfigFromImage retrieves that AuthConfig using the image string
   201  func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) {
   202  	registryRef, err := reference.ParseNormalizedNamed(image)
   203  	if err != nil {
   204  		return types.AuthConfig{}, err
   205  	}
   206  	repoInfo, err := registry.ParseRepositoryInfo(registryRef)
   207  	if err != nil {
   208  		return types.AuthConfig{}, err
   209  	}
   210  	return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil
   211  }