Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions cmd/lakectl/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@ package cmd

import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"slices"
"time"

"github.com/manifoldco/promptui"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/treeverse/lakefs/pkg/api/apigen"
"github.com/treeverse/lakefs/pkg/config"
"github.com/treeverse/lakefs/pkg/httputil"
)

const (
warningNotificationTmpl = `{{ . | yellow }}`
// TODO(ariels): Underline the link?
webLoginTemplate = `Opening {{.RedirectURL | blue | underline}} where you should log in.
If it does not open automatically, please try to open it manually and log in.
Expand All @@ -32,19 +38,99 @@ var (
loginRetryStatuses = slices.Concat(lakectlDefaultRetryStatuses,
[]int{http.StatusNotFound},
)

errNoProtocol = errors.New(`missing protocol, try e.g. "https://..."`)
errNoHost = errors.New(`missing host, e.g. "https://honey-badger.lakefscloud.us-east-1.io/"`)
)

func loginRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
return CheckRetry(ctx, resp, err, loginRetryStatuses)
}

func validateURL(maybeURL string) error {
u, err := url.Parse(maybeURL)
if err != nil {
return err
}
switch {
case u.Scheme == "":
return errNoProtocol
case u.Host == "":
return errNoHost
default:
return nil
}
}

func isIPAddress(hostname string) bool {
return net.ParseIP(hostname) != nil
}

func readLoginServerURL() (string, error) {
var (
ok = false
serverURL *url.URL
err error
)
Write(warningNotificationTmpl, "No .lakectl.yaml found. On lakeFS Enterprise, enter the server URL to log in.\n")
for !ok {
prompt := promptui.Prompt{
Label: "lakeFS server URL",
Validate: validateURL,
}
serverURLString, err := prompt.Run()
if err != nil {
return "", err
}
serverURL, err = url.Parse(serverURLString)
if err != nil { // Unlikely, validateURL should have done this!
return "", err
}

host := serverURL.Hostname()
if isIPAddress(host) {
prompt = promptui.Prompt{
IsConfirm: true,
Default: "n",
Label: "Numeric IP addresses will not work with OIDC; are you sure? ",
}
_, err := prompt.Run()
if err != nil && !errors.Is(err, promptui.ErrAbort) {
return "", err
}
ok = !errors.Is(err, promptui.ErrAbort)
} else {
ok = true
}
}
return serverURL.String(), err
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function returns err which is always nil at this point (from the previous url.Parse call that succeeded due to the comment on line 73). This should return nil explicitly instead of err to avoid confusion and potential future bugs if the code changes.

Suggested change
return serverURL.String(), err
return serverURL.String(), nil

Copilot uses AI. Check for mistakes.
}

func configureLogin(serverURL string) error {
viper.Set("server.endpoint_url", serverURL)
return viper.SafeWriteConfig()
}

var loginCmd = &cobra.Command{
Use: "login",
Short: "Use a web browser to log into lakeFS",
Long: "Connect to lakeFS using a web browser.",
Example: "lakectl login",
Run: func(cmd *cobra.Command, _ []string) {
serverURL, err := url.Parse(cfg.Server.EndpointURL.String())
var err error

writeConfigFile := false
serverEndpointURL := string(cfg.Server.EndpointURL)
if serverEndpointURL == "" {
serverEndpointURL, err = readLoginServerURL()
if err != nil {
DieFmt("No server endpoint URL: %s", err)
}
Write("Read URL {{. | yellow}}\n", serverEndpointURL)
cfg.Server.EndpointURL = config.OnlyString(serverEndpointURL)
writeConfigFile = true
}
serverURL, err := url.Parse(serverEndpointURL)
if err != nil {
DieErr(fmt.Errorf("get server URL %s: %w", cfg.Server.EndpointURL, err))
}
Expand Down Expand Up @@ -74,7 +160,7 @@ var loginCmd = &cobra.Command{
//
// TODO(ariels): The timeouts on some lakectl configurations may be too low for
// convenient login. Consider using a RetryClient based on a different
// configuration here..
// configuration here.
resp, err := client.GetTokenFromMailboxWithResponse(cmd.Context(), mailbox)

DieOnErrorOrUnexpectedStatusCode(resp, err, http.StatusOK)
Expand All @@ -91,6 +177,13 @@ var loginCmd = &cobra.Command{
}

Write(loggedInTemplate, struct{ Time string }{Time: time.Now().Format(time.DateTime)})

if writeConfigFile {
err = configureLogin(serverEndpointURL)
if err != nil {
Warning(fmt.Sprintf("Failed to save login configuration: %s.", err))
}
}
},
}

Expand Down
1 change: 0 additions & 1 deletion cmd/lakectl/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,6 @@ func initConfig() {
}

// set defaults
viper.SetDefault("server.endpoint_url", "http://127.0.0.1:8000")
viper.SetDefault("network.http2.enabled", defaultHTTP2Enabled)
viper.SetDefault("server.retries.enabled", true)
viper.SetDefault("server.retries.max_attempts", defaultMaxAttempts)
Expand Down
Loading