diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index a4e443bcaabd..215b610e36b8 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -189,6 +189,14 @@ Press ENTER to open your browser or submit your device code here: https://login. Waiting for authentication in the browser… ``` +Pressing ENTER opens the URL in your default browser. To use a +different command to open the URL, for example in a dev container that +forwards URLs to the host, set the `BROWSER` environment variable: + +```console +$ BROWSER="my-url-opener --flag" docker login +``` + After entering the code in your browser, you are authenticated to Docker Hub using the account you're currently signed in with on the Docker Hub website or in Docker Desktop. If you aren't signed in, you are prompted to sign in after diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 96deb61b1325..85f7adfd9b5f 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "os/exec" "strings" "github.com/docker/cli/cli/config/credentials" @@ -56,7 +57,7 @@ func New(options OAuthManagerOptions) *OAuthManager { // cluttering the output. We can handle errors ourselves. browser.Stdout = io.Discard browser.Stderr = io.Discard - openBrowser = browser.OpenURL + openBrowser = openURL } return &OAuthManager{ @@ -73,6 +74,21 @@ func New(options OAuthManagerOptions) *OAuthManager { } } +// openURL opens url using the command in the BROWSER environment variable +// if set, and falls back to the platform default otherwise, or if the +// command can't be started. +func openURL(url string) error { + if args := strings.Fields(os.Getenv("BROWSER")); len(args) > 0 { + cmd := exec.Command(args[0], append(args[1:], url)...) // #nosec G204 -- BROWSER is set by the user to choose the command to run + err := cmd.Start() + if err == nil { + return cmd.Wait() + } + logrus.WithError(err).Debug("failed to run BROWSER command, using default browser") + } + return browser.OpenURL(url) +} + var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login") // LoginDevice launches the device authentication flow with the tenant, diff --git a/internal/oauth/manager/manager_test.go b/internal/oauth/manager/manager_test.go index 44a744999b8b..471052d3877d 100644 --- a/internal/oauth/manager/manager_test.go +++ b/internal/oauth/manager/manager_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "os" + "path/filepath" + "runtime" "testing" "time" @@ -361,3 +363,17 @@ func (*fakeStore) GetFilename() string { func newStore(auths map[string]types.AuthConfig) *fakeStore { return &fakeStore{configs: auths} } + +func TestOpenURLWithBrowserEnv(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("uses touch as the browser command") + } + // "touch -m " creates a file at , which lets us check that + // the command (with its arguments) from BROWSER was used to open the url. + t.Setenv("BROWSER", "touch -m") + url := filepath.Join(t.TempDir(), "opened") + + assert.NilError(t, openURL(url)) + _, err := os.Stat(url) + assert.NilError(t, err) +}