diff --git a/cmd/lakebox/create.go b/cmd/lakebox/create.go index c6e5b7e1be..84034214e5 100644 --- a/cmd/lakebox/create.go +++ b/cmd/lakebox/create.go @@ -2,11 +2,13 @@ package lakebox import ( "encoding/json" + "errors" "fmt" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/spf13/cobra" ) @@ -68,10 +70,13 @@ Examples: _ = setGatewayHost(ctx, profile, result.GatewayHost) _ = upsertSandbox(ctx, profile, result.SandboxID, name) + // Only clobber an existing default if it's actually gone + // (404). Transient errors (5xx, network blip, rate limit) + // must not silently overwrite the user's chosen default. currentDefault := getDefault(ctx, profile) shouldSetDefault := currentDefault == "" - if !shouldSetDefault && currentDefault != "" { - if _, err := api.get(ctx, currentDefault); err != nil { + if !shouldSetDefault { + if _, err := api.get(ctx, currentDefault); errors.Is(err, apierr.ErrNotFound) { shouldSetDefault = true } } diff --git a/cmd/lakebox/delete.go b/cmd/lakebox/delete.go index f43c8eb735..91800700b7 100644 --- a/cmd/lakebox/delete.go +++ b/cmd/lakebox/delete.go @@ -76,7 +76,8 @@ Examples: return err } if !confirmed { - return errors.New("aborted") + cmdio.LogString(ctx, "Cancelled.") + return nil } } diff --git a/cmd/lakebox/keyhash.go b/cmd/lakebox/keyhash.go index 823eb1e168..238847fe44 100644 --- a/cmd/lakebox/keyhash.go +++ b/cmd/lakebox/keyhash.go @@ -3,6 +3,7 @@ package lakebox import ( "crypto/sha256" "encoding/hex" + "strings" ) // keyHash returns the identifier the lakebox SSH-keys API assigns to a @@ -10,7 +11,10 @@ import ( // the first 16 bytes and hex-encoded; the OpenSSH comment (anything after // the second whitespace-separated token) is stripped before hashing, so // registering the same key under different comments yields the same hash. +// Leading and trailing whitespace are trimmed first — `.pub` files end +// with a newline that would otherwise be hashed in for comment-less keys. func keyHash(publicKey string) string { + publicKey = strings.TrimSpace(publicKey) end := len(publicKey) spaces := 0 for i, c := range publicKey { diff --git a/cmd/lakebox/keyhash_test.go b/cmd/lakebox/keyhash_test.go index 638f1d8f34..32d98b310b 100644 --- a/cmd/lakebox/keyhash_test.go +++ b/cmd/lakebox/keyhash_test.go @@ -46,6 +46,19 @@ func TestKeyHash(t *testing.T) { input: "", want: "e3b0c44298fc1c149afbf4c8996fb924", }, + { + // `.pub` files end with a newline. Without trimming, a + // comment-less key would hash with `\n` mixed in and + // stop matching the server's value. + name: "trailing newline does not change hash", + input: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDUMMY\n", + want: "2b366430eb9743668b652921d3b22d54", + }, + { + name: "leading and trailing whitespace stripped", + input: " ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDUMMY \n", + want: "2b366430eb9743668b652921d3b22d54", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/cmd/lakebox/register.go b/cmd/lakebox/register.go index ca19a91865..9c7538f47d 100644 --- a/cmd/lakebox/register.go +++ b/cmd/lakebox/register.go @@ -85,7 +85,7 @@ Examples: // Remote-SSH and plain `ssh @lakebox-gw` both work without // the user pasting any config block (see maybeWriteSSHConfig). if err := maybeWriteSSHConfig(ctx, keyPath, w.Config.Host); err != nil { - warn(ctx, fmt.Sprintf("registered key, but failed to update ~/.ssh/config: %v", err)) + warn(ctx, fmt.Sprintf("Registered key, but failed to update ~/.ssh/config: %v", err)) } blank(stderr) diff --git a/cmd/lakebox/ssh.go b/cmd/lakebox/ssh.go index 73715bd204..d5768004ac 100644 --- a/cmd/lakebox/ssh.go +++ b/cmd/lakebox/ssh.go @@ -132,7 +132,7 @@ Examples: _ = clearDefault(ctx, profile) return fmt.Errorf("saved default %q no longer exists (cleared) — run `databricks lakebox create` to provision a new one, or `databricks lakebox default ` to point at an existing sandbox", def) default: - warn(ctx, fmt.Sprintf("could not validate default %s: %v", def, err)) + warn(ctx, fmt.Sprintf("Could not validate default %s: %v", def, err)) lakeboxID = def } } else { @@ -151,7 +151,7 @@ Examples: case errors.Is(err, apierr.ErrNotFound): return fmt.Errorf("no lakebox named %q — `databricks lakebox list` shows available IDs", lakeboxID) default: - warn(ctx, fmt.Sprintf("could not validate lakebox %s: %v", lakeboxID, err)) + warn(ctx, fmt.Sprintf("Could not validate lakebox %s: %v", lakeboxID, err)) } } @@ -221,7 +221,7 @@ func verifyKeyRegistered(ctx context.Context, api *lakeboxAPI, keyPath string) e keys, err := api.listKeys(ctx) if err != nil { - warn(ctx, fmt.Sprintf("could not verify SSH key registration: %v", err)) + warn(ctx, fmt.Sprintf("Could not verify SSH key registration: %v", err)) return nil } for _, k := range keys { diff --git a/cmd/lakebox/start.go b/cmd/lakebox/start.go index 6abb6cd486..1e1d230516 100644 --- a/cmd/lakebox/start.go +++ b/cmd/lakebox/start.go @@ -28,7 +28,7 @@ func newStartCommand() *cobra.Command { Long: `Start a stopped Lakebox environment. Boots the backing microVM and blocks until the sandbox reaches -Running (or up to 5 minutes). 'databricks lakebox ssh' already +Running (or up to 10 minutes). 'databricks lakebox ssh' already auto-starts a stopped sandbox on connection, so this command is mostly useful for pre-warming an environment without immediately connecting, or when a script needs to be sure the sandbox is up diff --git a/cmd/lakebox/status.go b/cmd/lakebox/status.go index 0a88b0b91c..5f149bd291 100644 --- a/cmd/lakebox/status.go +++ b/cmd/lakebox/status.go @@ -2,11 +2,13 @@ package lakebox import ( "encoding/json" + "errors" "fmt" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/spf13/cobra" ) @@ -44,6 +46,9 @@ Example: entry, err := api.get(ctx, lakeboxID) if err != nil { + if errors.Is(err, apierr.ErrNotFound) { + return fmt.Errorf("no lakebox named %q — `databricks lakebox list` shows available IDs", lakeboxID) + } return fmt.Errorf("failed to get lakebox %s: %w", lakeboxID, err) } @@ -66,9 +71,9 @@ Example: if entry.GatewayHost != "" { field(ctx, out, "gateway", cmdio.Faint(ctx, entry.GatewayHost)) } - if entry.FQDN != "" { - field(ctx, out, "fqdn", cmdio.Faint(ctx, entry.FQDN)) - } + // FQDN is the manager's internal routing host (per api.go); + // keep it in the JSON shape above for completeness but + // don't surface it to humans. field(ctx, out, "autostop", cmdio.Faint(ctx, entry.autoStopLabel())) blank(out) return nil diff --git a/cmd/lakebox/ui.go b/cmd/lakebox/ui.go index 203809ec98..2a2e9e924e 100644 --- a/cmd/lakebox/ui.go +++ b/cmd/lakebox/ui.go @@ -78,9 +78,10 @@ func ok(ctx context.Context, msg string) { cmdio.LogString(ctx, " "+cmdio.Cyan(ctx, "✓")+" "+msg) } -// warn prints " ! message" to stderr via the cmdio context. +// warn prints " ! message" to stderr via the cmdio context. Yellow so +// it visually differs from `ok`'s cyan ✓ and `spinner` cyan markers. func warn(ctx context.Context, msg string) { - cmdio.LogString(ctx, " "+cmdio.Cyan(ctx, "!")+" "+msg) + cmdio.LogString(ctx, " "+cmdio.Yellow(ctx, "!")+" "+msg) } // blank prints an empty line to w.