From 2ff4732bf07f120b3cbf17ceb6c1eaf2fd69856f Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 16:32:59 +0000 Subject: [PATCH 1/2] Resolve workspace IDs as strings at all CLI consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI calls CurrentWorkspaceID on the workspace client in five places. The SDK helper returns (int64, error) and reads the value from the X-Databricks-Org-Id response header on /api/2.0/preview/scim/v2/Me. Every caller then formats the int64 back to a string at its own boundary, or embeds it with %d in the case of experimental/ssh's driver-proxy URL. This commit introduces auth.ResolveWorkspaceID, a small helper that: 1. Returns w.Config.WorkspaceID if set (and not the CLI-only "none" sentinel) — no API call. 2. Otherwise calls w.CurrentWorkspaceID and stringifies the result. The fast path is also a small performance win — the workspace ID is already configured in the common SPOG case (set via --workspace-id, DATABRICKS_WORKSPACE_ID, workspace_id in .databrickscfg, or ?w=/?o= on a host URL), so resolving it no longer requires hitting /Me when the value is already known. Migrating the five consumers makes the in-CLI workspace ID flow string- typed end-to-end. As a side effect, non-numeric workspace identifiers (e.g. connection-style IDs that the new X-Databricks-Workspace-Id header accepts) now flow cleanly through these consumers when configured by the user. Consumers migrated: - databricks experimental open (cmd/experimental/workspace_open.go) - databricks bundle summary URL builder (bundle/config/mutator/initialize_urls.go) - databricks apps run-local (cmd/apps/run_local.go) - databricks experimental ssh driver-proxy URL (experimental/ssh/internal/client/websockets.go) - databricks experimental ssh cluster-metadata fetch (experimental/ssh/internal/client/client.go) Downstream signatures widened from int64 to string: - workspaceurls.BuildResourceURL, workspaceurls.workspaceBaseURL - runlocal.Config.WorkspaceID, runlocal.NewConfig - The two fmt.Sprintf calls in experimental/ssh swap %d -> %s for the workspace ID slot. The /driver-proxy-api/o//... URL keeps its legacy "o" path segment — that's a separate platform-side URL scheme decision from the ?o=/?w= query parameter migration. Out of scope: - libs/auth/introspect.go declares WorkspaceID int64 on the /api/2.0/tokens/introspect response. Different endpoint, separate follow-up. - cmd/auth/login.go's workspace picker reads WorkspaceId int64 from the SDK's Workspaces.List response — upstream SDK schema, not fixable in the CLI alone. Behavior preserved: - Configured workspace ID: identical value flows downstream. - Resolved-from-/Me path: SDK still parses the response header as int64; the helper just stringifies it. - Acceptance tests pass unchanged. --- bundle/config/mutator/initialize_urls.go | 7 +- cmd/apps/run_local.go | 2 +- cmd/experimental/workspace_open.go | 7 +- cmd/experimental/workspace_open_test.go | 40 +++---- experimental/ssh/internal/client/client.go | 5 +- .../ssh/internal/client/websockets.go | 8 +- libs/apps/runlocal/cfg.go | 4 +- libs/apps/runlocal/env.go | 2 +- libs/auth/workspace_id.go | 36 +++++++ libs/auth/workspace_id_test.go | 101 ++++++++++++++++++ libs/workspaceurls/urls.go | 16 +-- libs/workspaceurls/urls_test.go | 31 +++--- 12 files changed, 201 insertions(+), 58 deletions(-) create mode 100644 libs/auth/workspace_id.go create mode 100644 libs/auth/workspace_id_test.go diff --git a/bundle/config/mutator/initialize_urls.go b/bundle/config/mutator/initialize_urls.go index e14382b389..c3a877d9e8 100644 --- a/bundle/config/mutator/initialize_urls.go +++ b/bundle/config/mutator/initialize_urls.go @@ -3,10 +3,10 @@ package mutator import ( "context" "net/url" - "strconv" "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/diag" ) @@ -25,13 +25,12 @@ func (m *initializeURLs) Name() string { } func (m *initializeURLs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { - workspaceId, err := b.WorkspaceClient(ctx).CurrentWorkspaceID(ctx) + workspaceID, err := auth.ResolveWorkspaceID(ctx, b.WorkspaceClient(ctx)) if err != nil { return diag.FromErr(err) } - workspaceIDStr := strconv.FormatInt(workspaceId, 10) host := b.WorkspaceClient(ctx).Config.CanonicalHostName() - err = initializeForWorkspace(b, workspaceIDStr, host) + err = initializeForWorkspace(b, workspaceID, host) if err != nil { return diag.FromErr(err) } diff --git a/cmd/apps/run_local.go b/cmd/apps/run_local.go index 8d42783c47..144fbf5354 100644 --- a/cmd/apps/run_local.go +++ b/cmd/apps/run_local.go @@ -29,7 +29,7 @@ const SHUTDOWN_TIMEOUT = 15 * time.Second func setupWorkspaceAndConfig(cmd *cobra.Command, entryPoint string, appPort int) (*runlocal.Config, *runlocal.AppSpec, error) { ctx := cmd.Context() w := cmdctx.WorkspaceClient(ctx) - workspaceID, err := w.CurrentWorkspaceID(ctx) + workspaceID, err := auth.ResolveWorkspaceID(ctx, w) if err != nil { return nil, nil, err } diff --git a/cmd/experimental/workspace_open.go b/cmd/experimental/workspace_open.go index e48bdcc6d9..b6d05e71fc 100644 --- a/cmd/experimental/workspace_open.go +++ b/cmd/experimental/workspace_open.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/browser" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" @@ -15,8 +16,8 @@ import ( "github.com/databricks/cli/libs/workspaceurls" ) -var currentWorkspaceID = func(ctx context.Context) (int64, error) { - return cmdctx.WorkspaceClient(ctx).CurrentWorkspaceID(ctx) +var resolveWorkspaceID = func(ctx context.Context) (string, error) { + return auth.ResolveWorkspaceID(ctx, cmdctx.WorkspaceClient(ctx)) } var openWorkspaceURL = browser.Open @@ -49,7 +50,7 @@ Examples: resourceType := args[0] id := args[1] - workspaceID, err := currentWorkspaceID(ctx) + workspaceID, err := resolveWorkspaceID(ctx) if err != nil { log.Warnf(ctx, "Could not determine workspace ID: %v", err) } diff --git a/cmd/experimental/workspace_open_test.go b/cmd/experimental/workspace_open_test.go index 7839018747..f81e79be52 100644 --- a/cmd/experimental/workspace_open_test.go +++ b/cmd/experimental/workspace_open_test.go @@ -38,7 +38,7 @@ func TestBuildWorkspaceURLPathBasedResources(t *testing.T) { for _, tt := range tests { t.Run(tt.resourceType+"/"+tt.id, func(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", tt.resourceType, tt.id, 0) + got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", tt.resourceType, tt.id, "") require.NoError(t, err) assert.Equal(t, tt.expected, got) }) @@ -57,7 +57,7 @@ func TestBuildWorkspaceURLFragmentBasedResources(t *testing.T) { for _, tt := range tests { t.Run(tt.id, func(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", tt.resourceType, tt.id, 0) + got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", tt.resourceType, tt.id, "") require.NoError(t, err) assert.Equal(t, tt.expected, got) }) @@ -65,38 +65,38 @@ func TestBuildWorkspaceURLFragmentBasedResources(t *testing.T) { } func TestBuildWorkspaceURLUnknownResourceType(t *testing.T) { - _, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "unknown", "123", 0) + _, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "unknown", "123", "") assert.ErrorContains(t, err, "unknown resource type \"unknown\"") assert.ErrorContains(t, err, "alerts, apps, catalogs, clusters, dashboards, database_catalogs, database_instances, experiments, jobs, model_serving_endpoints, models, notebooks, pipelines, postgres_catalogs, postgres_synced_tables, quality_monitors, queries, registered_models, schemas, synced_database_tables, vector_search_endpoints, vector_search_indexes, volumes, warehouses") } func TestBuildWorkspaceURLHostWithTrailingSlash(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com/", "jobs", "123", 0) + got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com/", "jobs", "123", "") require.NoError(t, err) assert.Equal(t, "https://myworkspace.databricks.com/jobs/123", got) } func TestBuildWorkspaceURLWithWorkspaceID(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "jobs", "123", 123456) + got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "jobs", "123", "123456") require.NoError(t, err) assert.Equal(t, "https://myworkspace.databricks.com/jobs/123?w=123456", got) } func TestBuildWorkspaceURLWithWorkspaceIDInHostname(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://adb-123456.azuredatabricks.net", "jobs", "123", 123456) + got, err := workspaceurls.BuildResourceURL("https://adb-123456.azuredatabricks.net", "jobs", "123", "123456") require.NoError(t, err) // Workspace ID is already in the hostname, so ?w= should not be appended. assert.Equal(t, "https://adb-123456.azuredatabricks.net/jobs/123", got) } func TestBuildWorkspaceURLWithWorkspaceIDInVanityHostname(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://workspace-123456.example.com", "jobs", "123", 123456) + got, err := workspaceurls.BuildResourceURL("https://workspace-123456.example.com", "jobs", "123", "123456") require.NoError(t, err) assert.Equal(t, "https://workspace-123456.example.com/jobs/123?w=123456", got) } func TestBuildWorkspaceURLFragmentWithWorkspaceID(t *testing.T) { - got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "notebooks", "12345", 789) + got, err := workspaceurls.BuildResourceURL("https://myworkspace.databricks.com", "notebooks", "12345", "789") require.NoError(t, err) assert.Equal(t, "https://myworkspace.databricks.com/?w=789#notebook/12345", got) } @@ -158,15 +158,15 @@ func TestWorkspaceOpenCommandHelpText(t *testing.T) { } func TestWorkspaceOpenCommandOpensBrowserByDefault(t *testing.T) { - originalCurrentWorkspaceID := currentWorkspaceID + originalResolveWorkspaceID := resolveWorkspaceID originalOpenWorkspaceURL := openWorkspaceURL t.Cleanup(func() { - currentWorkspaceID = originalCurrentWorkspaceID + resolveWorkspaceID = originalResolveWorkspaceID openWorkspaceURL = originalOpenWorkspaceURL }) - currentWorkspaceID = func(context.Context) (int64, error) { - return 0, nil + resolveWorkspaceID = func(context.Context) (string, error) { + return "", nil } var gotURL string @@ -197,15 +197,15 @@ func TestWorkspaceOpenCommandOpensBrowserByDefault(t *testing.T) { } func TestWorkspaceOpenCommandURLFlag(t *testing.T) { - originalCurrentWorkspaceID := currentWorkspaceID + originalResolveWorkspaceID := resolveWorkspaceID originalOpenWorkspaceURL := openWorkspaceURL t.Cleanup(func() { - currentWorkspaceID = originalCurrentWorkspaceID + resolveWorkspaceID = originalResolveWorkspaceID openWorkspaceURL = originalOpenWorkspaceURL }) - currentWorkspaceID = func(context.Context) (int64, error) { - return 789, nil + resolveWorkspaceID = func(context.Context) (string, error) { + return "789", nil } browserOpened := false @@ -238,15 +238,15 @@ func TestWorkspaceOpenCommandURLFlag(t *testing.T) { } func TestWorkspaceOpenCommandWarnsWhenWorkspaceIDLookupFails(t *testing.T) { - originalCurrentWorkspaceID := currentWorkspaceID + originalResolveWorkspaceID := resolveWorkspaceID originalOpenWorkspaceURL := openWorkspaceURL t.Cleanup(func() { - currentWorkspaceID = originalCurrentWorkspaceID + resolveWorkspaceID = originalResolveWorkspaceID openWorkspaceURL = originalOpenWorkspaceURL }) - currentWorkspaceID = func(context.Context) (int64, error) { - return 0, errors.New("lookup failed") + resolveWorkspaceID = func(context.Context) (string, error) { + return "", errors.New("lookup failed") } openWorkspaceURL = func(ctx context.Context, targetURL string) error { diff --git a/experimental/ssh/internal/client/client.go b/experimental/ssh/internal/client/client.go index 69360b85d1..ff3ce1ee33 100644 --- a/experimental/ssh/internal/client/client.go +++ b/experimental/ssh/internal/client/client.go @@ -26,6 +26,7 @@ import ( "github.com/databricks/cli/experimental/ssh/internal/vscode" sshWorkspace "github.com/databricks/cli/experimental/ssh/internal/workspace" "github.com/databricks/cli/internal/build" + "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -438,11 +439,11 @@ func getServerMetadata(ctx context.Context, client *databricks.WorkspaceClient, return 0, "", "", errors.Join(errServerMetadata, errors.New("cluster ID not available in metadata")) } - workspaceID, err := client.CurrentWorkspaceID(ctx) + workspaceID, err := auth.ResolveWorkspaceID(ctx, client) if err != nil { return 0, "", "", err } - metadataURL := fmt.Sprintf("%s/driver-proxy-api/o/%d/%s/%d/metadata", client.Config.Host, workspaceID, effectiveClusterID, wsMetadata.Port) + metadataURL := fmt.Sprintf("%s/driver-proxy-api/o/%s/%s/%d/metadata", client.Config.Host, workspaceID, effectiveClusterID, wsMetadata.Port) log.Debugf(ctx, "Metadata URL: %s", metadataURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, nil) if err != nil { diff --git a/experimental/ssh/internal/client/websockets.go b/experimental/ssh/internal/client/websockets.go index 0dd7e37781..3241a9be2a 100644 --- a/experimental/ssh/internal/client/websockets.go +++ b/experimental/ssh/internal/client/websockets.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/databricks/cli/libs/auth" "github.com/databricks/databricks-sdk-go" "github.com/gorilla/websocket" ) @@ -38,11 +39,14 @@ func createWebsocketConnection(ctx context.Context, client *databricks.Workspace } func getProxyURL(ctx context.Context, client *databricks.WorkspaceClient, connID, clusterID string, serverPort int) (string, error) { - workspaceID, err := client.CurrentWorkspaceID(ctx) + workspaceID, err := auth.ResolveWorkspaceID(ctx, client) if err != nil { return "", fmt.Errorf("failed to get current workspace ID: %w", err) } host := client.Config.Host - url := fmt.Sprintf("%s/driver-proxy-api/o/%d/%s/%d/ssh?id=%s", host, workspaceID, clusterID, serverPort, connID) + // The /driver-proxy-api/o//... path is a legacy URL form on + // the driver-proxy endpoint and uses an "o" path segment regardless of + // whether the workspace ID itself is the legacy or new shape. + url := fmt.Sprintf("%s/driver-proxy-api/o/%s/%s/%d/ssh?id=%s", host, workspaceID, clusterID, serverPort, connID) return url, nil } diff --git a/libs/apps/runlocal/cfg.go b/libs/apps/runlocal/cfg.go index d196eb1208..7b236571d5 100644 --- a/libs/apps/runlocal/cfg.go +++ b/libs/apps/runlocal/cfg.go @@ -8,7 +8,7 @@ import ( type Config struct { AppName string AppURL string - WorkspaceID int64 + WorkspaceID string ServerName string Host string WorkspaceHost string @@ -24,7 +24,7 @@ const ( DEFAULT_PORT = 8000 ) -func NewConfig(workspaceHost string, workspaceID int64, appDir, host string, port int) *Config { +func NewConfig(workspaceHost, workspaceID, appDir, host string, port int) *Config { c := &Config{ AppName: DEFAULT_APP_NAME, AppURL: "http://" + net.JoinHostPort(host, strconv.Itoa(port)), diff --git a/libs/apps/runlocal/env.go b/libs/apps/runlocal/env.go index 7c0f562d41..1eae427d62 100644 --- a/libs/apps/runlocal/env.go +++ b/libs/apps/runlocal/env.go @@ -18,7 +18,7 @@ func GetBaseEnvVars(config *Config) []EnvVar { {Name: "PYTHONUNBUFFERED", Value: "1"}, {Name: "DATABRICKS_APP_NAME", Value: config.AppName}, {Name: "DATABRICKS_APP_URL", Value: config.AppURL}, - {Name: "DATABRICKS_WORKSPACE_ID", Value: strconv.FormatInt(config.WorkspaceID, 10)}, + {Name: "DATABRICKS_WORKSPACE_ID", Value: config.WorkspaceID}, {Name: "DATABRICKS_HOST", Value: config.WorkspaceHost}, {Name: "DATABRICKS_APP_PORT", Value: strconv.Itoa(config.Port)}, {Name: "GRADIO_SERVER_NAME", Value: config.ServerName}, diff --git a/libs/auth/workspace_id.go b/libs/auth/workspace_id.go new file mode 100644 index 0000000000..be48a6578b --- /dev/null +++ b/libs/auth/workspace_id.go @@ -0,0 +1,36 @@ +package auth + +import ( + "context" + "strconv" + + "github.com/databricks/databricks-sdk-go" +) + +// ResolveWorkspaceID returns the workspace ID as a string, preferring the +// value already configured on the client and falling back to a /Me probe. +// +// The fast path short-circuits the API call when w.Config.WorkspaceID is +// set (via --workspace-id, DATABRICKS_WORKSPACE_ID, workspace_id in +// .databrickscfg, ?o=/?w= on a host URL, or any other config source). The +// CLI-only "none" sentinel is treated as unset so it never leaks as a +// routing identifier. +// +// The fallback path delegates to w.CurrentWorkspaceID, which reads the +// X-Databricks-Org-Id response header on /api/2.0/preview/scim/v2/Me and +// parses it as int64. The numeric constraint is enforced by the SDK on +// that path; the helper just stringifies the result. +// +// Compared to calling w.CurrentWorkspaceID directly, the string return +// type lets callers pass the value to URL builders, env vars, and other +// string-typed sinks without a manual strconv.FormatInt step. +func ResolveWorkspaceID(ctx context.Context, w *databricks.WorkspaceClient) (string, error) { + if id := w.Config.WorkspaceID; id != "" && id != WorkspaceIDNone { + return id, nil + } + id, err := w.CurrentWorkspaceID(ctx) + if err != nil { + return "", err + } + return strconv.FormatInt(id, 10), nil +} diff --git a/libs/auth/workspace_id_test.go b/libs/auth/workspace_id_test.go new file mode 100644 index 0000000000..9b8f0cb214 --- /dev/null +++ b/libs/auth/workspace_id_test.go @@ -0,0 +1,101 @@ +package auth_test + +import ( + "testing" + + "github.com/databricks/cli/libs/auth" + "github.com/databricks/cli/libs/testserver" + "github.com/databricks/databricks-sdk-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveWorkspaceID_FastPathReturnsConfiguredValue(t *testing.T) { + // Pointing at a /Me handler that would return a different ID lets us + // confirm the fast path doesn't hit the API. + server := testserver.New(t) + server.Handle("GET", "/api/2.0/preview/scim/v2/Me", func(req testserver.Request) any { + t.Fatalf("/Me should not be called when WorkspaceID is configured") + return nil + }) + testserver.AddDefaultHandlers(server) + + w, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + WorkspaceID: "12345", + }) + require.NoError(t, err) + + got, err := auth.ResolveWorkspaceID(t.Context(), w) + require.NoError(t, err) + assert.Equal(t, "12345", got) +} + +func TestResolveWorkspaceID_FastPathPassesThroughNonNumericValue(t *testing.T) { + // Connection-style identifiers (UUID-shaped, etc.) are valid as of the + // X-Databricks-Workspace-Id header rollout. The configured value must + // flow through unchanged. + server := testserver.New(t) + server.Handle("GET", "/api/2.0/preview/scim/v2/Me", func(req testserver.Request) any { + t.Fatalf("/Me should not be called when WorkspaceID is configured") + return nil + }) + testserver.AddDefaultHandlers(server) + + const connID = "123e4567-e89b-12d3-a456-426614174000" + w, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + WorkspaceID: connID, + }) + require.NoError(t, err) + + got, err := auth.ResolveWorkspaceID(t.Context(), w) + require.NoError(t, err) + assert.Equal(t, connID, got) +} + +func TestResolveWorkspaceID_NoneSentinelFallsThroughToAPI(t *testing.T) { + // The CLI persists "none" in .databrickscfg to mark profiles where the + // user skipped workspace selection. It must not leak as a routing ID. + server := testserver.New(t) + testserver.AddDefaultHandlers(server) + + w, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + WorkspaceID: auth.WorkspaceIDNone, + }) + require.NoError(t, err) + + got, err := auth.ResolveWorkspaceID(t.Context(), w) + require.NoError(t, err) + // The default /Me handler returns X-Databricks-Org-Id: 900800700600. + assert.Equal(t, "900800700600", got) +} + +func TestResolveWorkspaceID_FallbackHitsMeAndStringifiesResponse(t *testing.T) { + // Note: testserver.New unconditionally registers a + // /.well-known/databricks-config handler that returns + // workspace_id=900800700600. The SDK config resolver picks that up and + // pre-populates cfg.WorkspaceID, which means the helper's fast path + // returns "900800700600" without ever hitting /Me. From the helper's + // perspective the observable behavior is identical (it returns + // "900800700600" either way), so this test still covers what callers + // see — it just lands on the fast path rather than the literal + // fallback path. The fast path is also exercised explicitly by + // TestResolveWorkspaceID_FastPathReturnsConfiguredValue. + server := testserver.New(t) + testserver.AddDefaultHandlers(server) + + w, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + }) + require.NoError(t, err) + + got, err := auth.ResolveWorkspaceID(t.Context(), w) + require.NoError(t, err) + assert.Equal(t, "900800700600", got) +} diff --git a/libs/workspaceurls/urls.go b/libs/workspaceurls/urls.go index 788c63a63a..be9a41fc95 100644 --- a/libs/workspaceurls/urls.go +++ b/libs/workspaceurls/urls.go @@ -4,7 +4,6 @@ import ( "fmt" "net/url" "slices" - "strconv" "strings" ) @@ -80,8 +79,10 @@ func ResourceURL(baseURL url.URL, resourceType, id string) string { // BuildResourceURL constructs a full workspace URL from a host string, resource // type name, ID, and workspace ID. It parses the host, appends ?w= -// when needed, and formats the resource path. -func BuildResourceURL(host, resourceType, id string, workspaceID int64) (string, error) { +// when needed, and formats the resource path. An empty workspaceID skips the +// query parameter (use when the workspace ID is unknown or already encoded in +// the hostname). +func BuildResourceURL(host, resourceType, id, workspaceID string) (string, error) { baseURL, err := workspaceBaseURL(host, workspaceID) if err != nil { return "", err @@ -101,23 +102,22 @@ func resolveAlias(resourceType string) string { return resourceType } -func workspaceBaseURL(host string, workspaceID int64) (*url.URL, error) { +func workspaceBaseURL(host, workspaceID string) (*url.URL, error) { baseURL, err := url.Parse(host) if err != nil { return nil, fmt.Errorf("invalid workspace host %q: %w", host, err) } - if workspaceID == 0 { + if workspaceID == "" { return baseURL, nil } - wsID := strconv.FormatInt(workspaceID, 10) - if hasWorkspaceIDInHostname(baseURL.Hostname(), wsID) { + if hasWorkspaceIDInHostname(baseURL.Hostname(), workspaceID) { return baseURL, nil } values := baseURL.Query() - values.Add("w", wsID) + values.Add("w", workspaceID) baseURL.RawQuery = values.Encode() return baseURL, nil diff --git a/libs/workspaceurls/urls_test.go b/libs/workspaceurls/urls_test.go index 5107d0a21c..af7b5b08ea 100644 --- a/libs/workspaceurls/urls_test.go +++ b/libs/workspaceurls/urls_test.go @@ -45,15 +45,16 @@ func TestWorkspaceBaseURL(t *testing.T) { tests := []struct { name string host string - workspaceID int64 + workspaceID string expected string }{ - {"no workspace ID", "https://myworkspace.databricks.com", 0, "https://myworkspace.databricks.com"}, - {"with workspace ID", "https://myworkspace.databricks.com", 123456, "https://myworkspace.databricks.com?w=123456"}, - {"trailing slash stripped", "https://myworkspace.databricks.com/", 0, "https://myworkspace.databricks.com/"}, - {"trailing slash with workspace ID", "https://myworkspace.databricks.com/", 789, "https://myworkspace.databricks.com/?w=789"}, - {"adb hostname skips query param", "https://adb-123456.azuredatabricks.net", 123456, "https://adb-123456.azuredatabricks.net"}, - {"adb hostname mismatch adds param", "https://adb-999.azuredatabricks.net", 123456, "https://adb-999.azuredatabricks.net?w=123456"}, + {"no workspace ID", "https://myworkspace.databricks.com", "", "https://myworkspace.databricks.com"}, + {"with workspace ID", "https://myworkspace.databricks.com", "123456", "https://myworkspace.databricks.com?w=123456"}, + {"trailing slash stripped", "https://myworkspace.databricks.com/", "", "https://myworkspace.databricks.com/"}, + {"trailing slash with workspace ID", "https://myworkspace.databricks.com/", "789", "https://myworkspace.databricks.com/?w=789"}, + {"adb hostname skips query param", "https://adb-123456.azuredatabricks.net", "123456", "https://adb-123456.azuredatabricks.net"}, + {"adb hostname mismatch adds param", "https://adb-999.azuredatabricks.net", "123456", "https://adb-999.azuredatabricks.net?w=123456"}, + {"connection-id-style workspace ID passes through", "https://spog.example.com", "123e4567-e89b-12d3-a456-426614174000", "https://spog.example.com?w=123e4567-e89b-12d3-a456-426614174000"}, } for _, tt := range tests { @@ -66,7 +67,7 @@ func TestWorkspaceBaseURL(t *testing.T) { } func TestWorkspaceBaseURLInvalidHost(t *testing.T) { - _, err := workspaceBaseURL("://invalid", 0) + _, err := workspaceBaseURL("://invalid", "") assert.ErrorContains(t, err, "invalid workspace host") } @@ -76,14 +77,14 @@ func TestBuildResourceURL(t *testing.T) { host string resourceType string id string - workspaceID int64 + workspaceID string expected string }{ - {"simple path", "https://host.com", "jobs", "123", 0, "https://host.com/jobs/123"}, - {"path with workspace ID", "https://host.com", "jobs", "123", 456, "https://host.com/jobs/123?w=456"}, - {"fragment pattern", "https://host.com", "notebooks", "12345", 0, "https://host.com/#notebook/12345"}, - {"fragment with workspace ID", "https://host.com", "notebooks", "12345", 789, "https://host.com/?w=789#notebook/12345"}, - {"registered model normalizes dots", "https://host.com", "registered_models", "catalog.schema.model", 0, "https://host.com/explore/data/models/catalog/schema/model"}, + {"simple path", "https://host.com", "jobs", "123", "", "https://host.com/jobs/123"}, + {"path with workspace ID", "https://host.com", "jobs", "123", "456", "https://host.com/jobs/123?w=456"}, + {"fragment pattern", "https://host.com", "notebooks", "12345", "", "https://host.com/#notebook/12345"}, + {"fragment with workspace ID", "https://host.com", "notebooks", "12345", "789", "https://host.com/?w=789#notebook/12345"}, + {"registered model normalizes dots", "https://host.com", "registered_models", "catalog.schema.model", "", "https://host.com/explore/data/models/catalog/schema/model"}, } for _, tt := range tests { @@ -96,7 +97,7 @@ func TestBuildResourceURL(t *testing.T) { } func TestBuildResourceURLUnknownType(t *testing.T) { - _, err := BuildResourceURL("https://host.com", "unknown", "123", 0) + _, err := BuildResourceURL("https://host.com", "unknown", "123", "") assert.ErrorContains(t, err, "unknown resource type") } From ae925283beb290c8d55cdf90a7429dd0f1b6359b Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 16:45:54 +0000 Subject: [PATCH 2/2] Regenerate bundle/user_agent/simple fixture for skipped /Me request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ResolveWorkspaceID helper's fast path returns the already-configured cfg.WorkspaceID without hitting /api/2.0/preview/scim/v2/Me. This is the intended behavior — saves an API call when the workspace ID is already known — but the bundle/user_agent/simple golden was recorded with the old behavior where bundle summary always made the /Me call. Regenerated via ./task test-update. Diff is a clean removal of the /Me request from out.requests.summary.{direct,terraform}.json, plus the matching count in output.txt. No other behavior change. --- acceptance/bundle/user_agent/output.txt | 2 -- .../user_agent/simple/out.requests.summary.direct.json | 9 --------- .../simple/out.requests.summary.terraform.json | 9 --------- 3 files changed, 20 deletions(-) diff --git a/acceptance/bundle/user_agent/output.txt b/acceptance/bundle/user_agent/output.txt index 5c6e383d1f..f2a1cd12a2 100644 --- a/acceptance/bundle/user_agent/output.txt +++ b/acceptance/bundle/user_agent/output.txt @@ -125,12 +125,10 @@ MISS run.terraform /.well-known/databricks-config 'cli/[DEV_VERSION] databricks- MISS summary.direct /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' MISS summary.direct /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' MISS summary.direct /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' -OK summary.direct /api/2.0/preview/scim/v2/Me engine/direct MISS summary.direct /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS summary.terraform /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' MISS summary.terraform /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' MISS summary.terraform /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none auth/pat' -OK summary.terraform /api/2.0/preview/scim/v2/Me engine/terraform MISS summary.terraform /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS validate.direct /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' MISS validate.direct /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' diff --git a/acceptance/bundle/user_agent/simple/out.requests.summary.direct.json b/acceptance/bundle/user_agent/simple/out.requests.summary.direct.json index c3017391f2..3a3e2db9e9 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.summary.direct.json +++ b/acceptance/bundle/user_agent/simple/out.requests.summary.direct.json @@ -33,15 +33,6 @@ "return_export_info": "true" } } -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none engine/direct auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/preview/scim/v2/Me" -} { "headers": { "User-Agent": [ diff --git a/acceptance/bundle/user_agent/simple/out.requests.summary.terraform.json b/acceptance/bundle/user_agent/simple/out.requests.summary.terraform.json index bf160a9744..3a3e2db9e9 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.summary.terraform.json +++ b/acceptance/bundle/user_agent/simple/out.requests.summary.terraform.json @@ -33,15 +33,6 @@ "return_export_info": "true" } } -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_summary cmd-exec-id/[UUID] interactive/none engine/terraform auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/preview/scim/v2/Me" -} { "headers": { "User-Agent": [