From f1cbbd1891a621f9524cbd69c90b4f30cefa5cce Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Thu, 28 May 2026 13:53:44 +0000 Subject: [PATCH 1/4] Send X-Databricks-Workspace-Id on workspace-routed API calls On unified Databricks hosts that serve multiple workspaces, the CLI sends a routing header so the gateway can dispatch each request to the correct workspace. The platform is consolidating workspace addressing onto a single header name. Switch the header sent on workspace-scoped API calls from X-Databricks-Org-Id to X-Databricks-Workspace-Id. The previous header continues to be accepted by the platform, so older CLI versions keep working. This change covers the hand-written paths in the CLI that issue workspace- routed API calls without going through generated SDK service methods: - databricks api {verb} (cmd/api/api.go) - Files API filer (libs/filer/files_client.go) - Workspace files filer (libs/filer/workspace_files_client.go) - Telemetry uploads (libs/telemetry/logger.go) The value continues to come from the workspace_id config field or DATABRICKS_WORKSPACE_ID. The CurrentWorkspaceID() helper in the SDK still reads X-Databricks-Org-Id from the response on /api/2.0/preview/scim/v2/Me, so the response-side references in libs/testserver/handlers.go and libs/testproxy/server.go intentionally remain on the legacy header name. --- acceptance/cmd/api/account-flag/script | 2 +- acceptance/cmd/api/account-path/script | 2 +- acceptance/cmd/api/default-profile/output.txt | 2 +- acceptance/cmd/api/default-profile/script | 2 +- acceptance/cmd/api/test.toml | 2 +- acceptance/cmd/api/workspace-id-flag/output.txt | 2 +- acceptance/cmd/api/workspace-id-flag/script | 2 +- .../cmd/api/workspace-id-from-query/output.txt | 2 +- acceptance/cmd/api/workspace-id-from-query/script | 2 +- acceptance/cmd/api/workspace-id-none/script | 2 +- acceptance/cmd/api/workspace-path/output.txt | 2 +- acceptance/cmd/api/workspace-path/script | 2 +- .../cmd/api/workspace-proxy-regression/output.txt | 2 +- .../cmd/api/workspace-proxy-regression/script | 2 +- acceptance/telemetry/failure/out.requests.txt | 6 +++--- .../telemetry/partial-success/out.requests.txt | 6 +++--- acceptance/telemetry/success/out.requests.txt | 8 ++++---- acceptance/telemetry/test.toml | 2 +- acceptance/telemetry/timeout/out.requests.txt | 2 +- cmd/api/api.go | 12 +++++++----- libs/filer/files_client.go | 14 +++++++------- libs/filer/workspace_files_client.go | 14 +++++++------- libs/filer/workspace_files_client_test.go | 8 ++++---- libs/telemetry/logger.go | 14 +++++++------- 24 files changed, 58 insertions(+), 56 deletions(-) diff --git a/acceptance/cmd/api/account-flag/script b/acceptance/cmd/api/account-flag/script index de2d4de92b7..b276923914b 100644 --- a/acceptance/cmd/api/account-flag/script +++ b/acceptance/cmd/api/account-flag/script @@ -1,2 +1,2 @@ MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/clusters/list --account -trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Workspace-Id" diff --git a/acceptance/cmd/api/account-path/script b/acceptance/cmd/api/account-path/script index 6cc97637695..cb84c2bccf3 100644 --- a/acceptance/cmd/api/account-path/script +++ b/acceptance/cmd/api/account-path/script @@ -1,2 +1,2 @@ MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/accounts/abc-123/network-policies -trace print_requests.py --get //api/2.0/accounts/abc-123/network-policies | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/accounts/abc-123/network-policies | contains.py "!X-Databricks-Workspace-Id" diff --git a/acceptance/cmd/api/default-profile/output.txt b/acceptance/cmd/api/default-profile/output.txt index 2d22f0db2bc..882b01befac 100644 --- a/acceptance/cmd/api/default-profile/output.txt +++ b/acceptance/cmd/api/default-profile/output.txt @@ -13,7 +13,7 @@ "User-Agent": [ "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/acceptance/cmd/api/default-profile/script b/acceptance/cmd/api/default-profile/script index 0a926912fa0..44f93db28c7 100644 --- a/acceptance/cmd/api/default-profile/script +++ b/acceptance/cmd/api/default-profile/script @@ -22,7 +22,7 @@ unset DATABRICKS_CONFIG_PROFILE title "default_profile is used when no --profile flag and no DATABRICKS_CONFIG_PROFILE\n" MSYS_NO_PATHCONV=1 trace $CLI api get /api/2.0/clusters/list -trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" title "--profile overrides default_profile (negative case)\n" MSYS_NO_PATHCONV=1 errcode $CLI api get /api/2.0/clusters/list -p other 2>&1 | contains.py "other.test" diff --git a/acceptance/cmd/api/test.toml b/acceptance/cmd/api/test.toml index 11d83c3f486..d1a9112ed0e 100644 --- a/acceptance/cmd/api/test.toml +++ b/acceptance/cmd/api/test.toml @@ -1,5 +1,5 @@ RecordRequests = true -IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Org-Id"] +IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Workspace-Id"] # Normalize OS-dependent and CI-only User-Agent segments so the recorded # requests are stable across local macOS/Linux runs and GitHub Actions. diff --git a/acceptance/cmd/api/workspace-id-flag/output.txt b/acceptance/cmd/api/workspace-id-flag/output.txt index 5ff264fa554..2ed9f181211 100644 --- a/acceptance/cmd/api/workspace-id-flag/output.txt +++ b/acceptance/cmd/api/workspace-id-flag/output.txt @@ -9,7 +9,7 @@ "User-Agent": [ "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "999" ] }, diff --git a/acceptance/cmd/api/workspace-id-flag/script b/acceptance/cmd/api/workspace-id-flag/script index 83664ba8662..060a6fafbcb 100644 --- a/acceptance/cmd/api/workspace-id-flag/script +++ b/acceptance/cmd/api/workspace-id-flag/script @@ -1,2 +1,2 @@ MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/clusters/list --workspace-id 999 -trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-from-query/output.txt b/acceptance/cmd/api/workspace-id-from-query/output.txt index 7a72f8de473..f62e2c45f57 100644 --- a/acceptance/cmd/api/workspace-id-from-query/output.txt +++ b/acceptance/cmd/api/workspace-id-from-query/output.txt @@ -9,7 +9,7 @@ "User-Agent": [ "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "999" ] }, diff --git a/acceptance/cmd/api/workspace-id-from-query/script b/acceptance/cmd/api/workspace-id-from-query/script index fd3fa0e151b..ff74526bf88 100644 --- a/acceptance/cmd/api/workspace-id-from-query/script +++ b/acceptance/cmd/api/workspace-id-from-query/script @@ -1,2 +1,2 @@ MSYS_NO_PATHCONV=1 $CLI api get "/api/2.0/clusters/list?o=999" -trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-none/script b/acceptance/cmd/api/workspace-id-none/script index 4ecd00c6768..9e01e4da2d4 100644 --- a/acceptance/cmd/api/workspace-id-none/script +++ b/acceptance/cmd/api/workspace-id-none/script @@ -10,4 +10,4 @@ workspace_id = none EOF MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/clusters/list --profile spog-account -trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Workspace-Id" diff --git a/acceptance/cmd/api/workspace-path/output.txt b/acceptance/cmd/api/workspace-path/output.txt index 9d17f66284f..e4685830e47 100644 --- a/acceptance/cmd/api/workspace-path/output.txt +++ b/acceptance/cmd/api/workspace-path/output.txt @@ -9,7 +9,7 @@ "User-Agent": [ "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/acceptance/cmd/api/workspace-path/script b/acceptance/cmd/api/workspace-path/script index 4e5bb35c4be..16351c0ef07 100644 --- a/acceptance/cmd/api/workspace-path/script +++ b/acceptance/cmd/api/workspace-path/script @@ -1,2 +1,2 @@ MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/clusters/list -trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" diff --git a/acceptance/cmd/api/workspace-proxy-regression/output.txt b/acceptance/cmd/api/workspace-proxy-regression/output.txt index c98486a15e1..66cbd75022a 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/output.txt +++ b/acceptance/cmd/api/workspace-proxy-regression/output.txt @@ -9,7 +9,7 @@ "User-Agent": [ "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/acceptance/cmd/api/workspace-proxy-regression/script b/acceptance/cmd/api/workspace-proxy-regression/script index 39ab661f4f1..8620765476d 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/script +++ b/acceptance/cmd/api/workspace-proxy-regression/script @@ -2,4 +2,4 @@ # being misclassified as account-scope, so the routing identifier should be # present on the recorded request. MSYS_NO_PATHCONV=1 $CLI api get /api/2.0/preview/accounts/access-control/rule-sets -trace print_requests.py --get //api/2.0/preview/accounts/access-control/rule-sets | contains.py "X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/preview/accounts/access-control/rule-sets | contains.py "X-Databricks-Workspace-Id" diff --git a/acceptance/telemetry/failure/out.requests.txt b/acceptance/telemetry/failure/out.requests.txt index 808b5e97243..11c7bffe1ce 100644 --- a/acceptance/telemetry/failure/out.requests.txt +++ b/acceptance/telemetry/failure/out.requests.txt @@ -7,7 +7,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, @@ -27,7 +27,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, @@ -47,7 +47,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/acceptance/telemetry/partial-success/out.requests.txt b/acceptance/telemetry/partial-success/out.requests.txt index 808b5e97243..11c7bffe1ce 100644 --- a/acceptance/telemetry/partial-success/out.requests.txt +++ b/acceptance/telemetry/partial-success/out.requests.txt @@ -7,7 +7,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, @@ -27,7 +27,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, @@ -47,7 +47,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/acceptance/telemetry/success/out.requests.txt b/acceptance/telemetry/success/out.requests.txt index f73cd0a891e..10f155b0654 100644 --- a/acceptance/telemetry/success/out.requests.txt +++ b/acceptance/telemetry/success/out.requests.txt @@ -13,9 +13,9 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/selftest_send-telemetry cmd-exec-id/[CMD-EXEC-ID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/selftest_send-telemetry cmd-exec-id/[UUID] interactive/none auth/pat" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, @@ -25,8 +25,8 @@ "uploadTime": [UNIX_TIME_MILLIS], "items": [], "protoLogs": [ - "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[CMD-EXEC-ID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE1\"}}}}", - "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[CMD-EXEC-ID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE2\"}}}}" + "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE1\"}}}}", + "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE2\"}}}}" ] } } diff --git a/acceptance/telemetry/test.toml b/acceptance/telemetry/test.toml index cce01f8ccd2..f6f033b91ee 100644 --- a/acceptance/telemetry/test.toml +++ b/acceptance/telemetry/test.toml @@ -1,4 +1,4 @@ -IncludeRequestHeaders = ["Authorization", "X-Databricks-Org-Id"] +IncludeRequestHeaders = ["Authorization", "X-Databricks-Workspace-Id"] RecordRequests = true Local = true diff --git a/acceptance/telemetry/timeout/out.requests.txt b/acceptance/telemetry/timeout/out.requests.txt index 1241a933a9a..89556e1dcb7 100644 --- a/acceptance/telemetry/timeout/out.requests.txt +++ b/acceptance/telemetry/timeout/out.requests.txt @@ -7,7 +7,7 @@ "Authorization": [ "Bearer [DATABRICKS_TOKEN]" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, diff --git a/cmd/api/api.go b/cmd/api/api.go index ab70ca8b753..2f64f49e668 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -20,10 +20,12 @@ import ( ) const ( - // orgIDHeader is the workspace routing identifier sent on workspace-scope - // requests against unified hosts. Generated SDK service methods set this - // per-call when cfg.WorkspaceID is populated; we mirror the same idiom. - orgIDHeader = "X-Databricks-Org-Id" + // workspaceIDHeader is the workspace routing identifier sent on + // workspace-scope requests against unified hosts. Generated SDK service + // methods set this per-call when cfg.WorkspaceID is populated; we mirror + // the same idiom. The gateway also accepts the legacy X-Databricks-Org-Id + // header for rollback safety. + workspaceIDHeader = "X-Databricks-Workspace-Id" // orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used // by the Databricks UI: "?o=" identifies the workspace a URL @@ -112,7 +114,7 @@ func makeCommand(method string) *cobra.Command { headers := map[string]string{"Content-Type": "application/json"} if orgID != "" { - headers[orgIDHeader] = orgID + headers[workspaceIDHeader] = orgID } var response any diff --git a/libs/filer/files_client.go b/libs/filer/files_client.go index 2ac76166162..5c8c6a84a52 100644 --- a/libs/filer/files_client.go +++ b/libs/filer/files_client.go @@ -109,16 +109,16 @@ func NewFilesClient(w *databricks.WorkspaceClient, root string) (Filer, error) { }, nil } -// orgIDHeaders returns headers with X-Databricks-Org-Id set if a workspace ID -// is configured. SPOG hosts require this header to route requests to the -// correct workspace. -func (w *FilesClient) orgIDHeaders() map[string]string { +// workspaceIDHeaders returns headers with X-Databricks-Workspace-Id set if a +// workspace ID is configured. SPOG hosts require this header to route requests +// to the correct workspace. +func (w *FilesClient) workspaceIDHeaders() map[string]string { wsID := w.workspaceClient.Config.WorkspaceID if wsID == "" { return nil } return map[string]string{ - "X-Databricks-Org-Id": wsID, + "X-Databricks-Workspace-Id": wsID, } } @@ -162,7 +162,7 @@ func (w *FilesClient) Write(ctx context.Context, name string, reader io.Reader, urlPath = fmt.Sprintf("%s?overwrite=%t", urlPath, overwrite) headers := map[string]string{"Content-Type": "application/octet-stream"} if wsID := w.workspaceClient.Config.WorkspaceID; wsID != "" { - headers["X-Databricks-Org-Id"] = wsID + headers["X-Databricks-Workspace-Id"] = wsID } err = w.apiClient.Do(ctx, http.MethodPut, urlPath, headers, nil, reader, nil) @@ -192,7 +192,7 @@ func (w *FilesClient) Read(ctx context.Context, name string) (io.ReadCloser, err } var reader io.ReadCloser - err = w.apiClient.Do(ctx, http.MethodGet, urlPath, w.orgIDHeaders(), nil, nil, &reader) + err = w.apiClient.Do(ctx, http.MethodGet, urlPath, w.workspaceIDHeaders(), nil, nil, &reader) // Return early on success. if err == nil { diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index a23c97724cf..65aff218cda 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -121,10 +121,10 @@ type WorkspaceFilesClient struct { root WorkspaceRootPath } -// orgIDHeaders returns headers with X-Databricks-Org-Id set if a workspace ID -// is configured. SPOG hosts require this header to route requests to the -// correct workspace. -func (w *WorkspaceFilesClient) orgIDHeaders() map[string]string { +// workspaceIDHeaders returns headers with X-Databricks-Workspace-Id set if a +// workspace ID is configured. SPOG hosts require this header to route requests +// to the correct workspace. +func (w *WorkspaceFilesClient) workspaceIDHeaders() map[string]string { if w.workspaceClient == nil || w.workspaceClient.Config == nil { return nil } @@ -133,7 +133,7 @@ func (w *WorkspaceFilesClient) orgIDHeaders() map[string]string { return nil } return map[string]string{ - "X-Databricks-Org-Id": wsID, + "X-Databricks-Workspace-Id": wsID, } } @@ -171,7 +171,7 @@ func (w *WorkspaceFilesClient) Write(ctx context.Context, name string, reader io return err } - err = w.apiClient.Do(ctx, http.MethodPost, urlPath, w.orgIDHeaders(), nil, body, nil) + err = w.apiClient.Do(ctx, http.MethodPost, urlPath, w.workspaceIDHeaders(), nil, body, nil) // Return early on success. if err == nil { @@ -349,7 +349,7 @@ func (w *WorkspaceFilesClient) Stat(ctx context.Context, name string) (fs.FileIn ctx, http.MethodGet, "/api/2.0/workspace/get-status", - w.orgIDHeaders(), + w.workspaceIDHeaders(), nil, map[string]string{ "path": absPath, diff --git a/libs/filer/workspace_files_client_test.go b/libs/filer/workspace_files_client_test.go index e7b5a337a7f..2915af60436 100644 --- a/libs/filer/workspace_files_client_test.go +++ b/libs/filer/workspace_files_client_test.go @@ -61,7 +61,7 @@ func TestWorkspaceFilesDirEntry(t *testing.T) { assert.True(t, i2.IsDir()) } -func TestWorkspaceFilesClientOrgIDHeaders(t *testing.T) { +func TestWorkspaceFilesClientWorkspaceIDHeaders(t *testing.T) { tests := []struct { name string workspaceID string @@ -70,7 +70,7 @@ func TestWorkspaceFilesClientOrgIDHeaders(t *testing.T) { { name: "with workspace ID", workspaceID: "7474644166319138", - expect: map[string]string{"X-Databricks-Org-Id": "7474644166319138"}, + expect: map[string]string{"X-Databricks-Workspace-Id": "7474644166319138"}, }, { name: "without workspace ID", @@ -87,13 +87,13 @@ func TestWorkspaceFilesClientOrgIDHeaders(t *testing.T) { }, }, } - assert.Equal(t, tc.expect, w.orgIDHeaders()) + assert.Equal(t, tc.expect, w.workspaceIDHeaders()) }) } t.Run("nil workspace client", func(t *testing.T) { w := &WorkspaceFilesClient{} - assert.Nil(t, w.orgIDHeaders()) + assert.Nil(t, w.workspaceIDHeaders()) }) } diff --git a/libs/telemetry/logger.go b/libs/telemetry/logger.go index d56e087322e..7ef62ef03f2 100644 --- a/libs/telemetry/logger.go +++ b/libs/telemetry/logger.go @@ -171,23 +171,23 @@ func Upload(ctx context.Context, ec protos.ExecutionContext) error { return errors.New("failed to upload telemetry logs after three attempts") } -// orgIDHeaders returns headers with X-Databricks-Org-Id set if a workspace ID -// is configured. SPOG hosts require this header to route requests to the -// correct workspace; without it, telemetry is recorded in a central shard -// instead of the correct workspace. -func orgIDHeaders(apiClient *client.DatabricksClient) map[string]string { +// workspaceIDHeaders returns headers with X-Databricks-Workspace-Id set if a +// workspace ID is configured. SPOG hosts require this header to route requests +// to the correct workspace; without it, telemetry is recorded in a central +// shard instead of the correct workspace. +func workspaceIDHeaders(apiClient *client.DatabricksClient) map[string]string { wsID := apiClient.Config.WorkspaceID if wsID == "" { return nil } return map[string]string{ - "X-Databricks-Org-Id": wsID, + "X-Databricks-Workspace-Id": wsID, } } func attempt(ctx context.Context, apiClient *client.DatabricksClient, protoLogs []string) (*ResponseBody, error) { resp := &ResponseBody{} - err := apiClient.Do(ctx, http.MethodPost, "/telemetry-ext", orgIDHeaders(apiClient), nil, RequestBody{ + err := apiClient.Do(ctx, http.MethodPost, "/telemetry-ext", workspaceIDHeaders(apiClient), nil, RequestBody{ UploadTime: time.Now().UnixMilli(), // There is a bug in the `/telemetry-ext` API which requires us to // send an empty array for the `Items` field. Otherwise the API returns From a54149d20f5d85791454ef90a63c0f7057eaccef Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 08:11:34 +0000 Subject: [PATCH 2/4] Fix telemetry/success golden file placeholder The cmd_exec_id placeholder regressed from [CMD-EXEC-ID] to [UUID] when the fixture was regenerated locally with jq 1.6. The test script uses jq '.headers."User-Agent".[0]' syntax that only works in jq 1.7+; on the older runtime that pipeline silently produced no replacement, leaving the raw UUID in place which then matched the generic UUID replacement. Re-regenerated with jq 1.7. CI was already running 1.7+ which is why the discrepancy only showed up against the wire output, not locally. --- acceptance/telemetry/success/out.requests.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/acceptance/telemetry/success/out.requests.txt b/acceptance/telemetry/success/out.requests.txt index 10f155b0654..01267fa3a72 100644 --- a/acceptance/telemetry/success/out.requests.txt +++ b/acceptance/telemetry/success/out.requests.txt @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/selftest_send-telemetry cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/selftest_send-telemetry cmd-exec-id/[CMD-EXEC-ID] interactive/none auth/pat" ], "X-Databricks-Workspace-Id": [ "[NUMID]" @@ -25,8 +25,8 @@ "uploadTime": [UNIX_TIME_MILLIS], "items": [], "protoLogs": [ - "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE1\"}}}}", - "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE2\"}}}}" + "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[CMD-EXEC-ID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE1\"}}}}", + "{\"frontend_log_event_id\":\"[UUID]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[CMD-EXEC-ID]\",\"version\":\"[DEV_VERSION]\",\"command\":\"selftest_send-telemetry\",\"operating_system\":\"[OS]\",\"execution_time_ms\":\"SMALL_INT\",\"exit_code\":0},\"cli_test_event\":{\"name\":\"VALUE2\"}}}}" ] } } From c579a1c4b3188ed0189a86db078772ccc3bfbdbb Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia Date: Fri, 29 May 2026 13:20:51 +0000 Subject: [PATCH 3/4] Extend header flip to bundle deployment state filer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bundle/deploy/filer.go has its own stateFiler that fetches the bundle deployment state from /api/2.0/workspace-files/ using client.Do() directly (bypassing generated SDK service methods). Apply the same X-Databricks-Org-Id → X-Databricks-Workspace-Id swap here so bundle deploy/summary against unified hosts uses the new routing header consistently with the rest of the CLI. Method renamed orgIDHeaders → workspaceIDHeaders for consistency with the other filer helpers updated in this PR. --- bundle/deploy/filer.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundle/deploy/filer.go b/bundle/deploy/filer.go index a6b36f8d04e..e773df4e0e0 100644 --- a/bundle/deploy/filer.go +++ b/bundle/deploy/filer.go @@ -25,16 +25,16 @@ type stateFiler struct { root filer.WorkspaceRootPath } -// orgIDHeaders returns headers with X-Databricks-Org-Id set if a workspace ID -// is configured. SPOG hosts require this header to route requests to the -// correct workspace. -func (s stateFiler) orgIDHeaders() map[string]string { +// workspaceIDHeaders returns headers with X-Databricks-Workspace-Id set if a +// workspace ID is configured. SPOG hosts require this header to route requests +// to the correct workspace. +func (s stateFiler) workspaceIDHeaders() map[string]string { wsID := s.apiClient.Config.WorkspaceID if wsID == "" { return nil } return map[string]string{ - "X-Databricks-Org-Id": wsID, + "X-Databricks-Workspace-Id": wsID, } } @@ -63,7 +63,7 @@ func (s stateFiler) Read(ctx context.Context, path string) (io.ReadCloser, error var buf bytes.Buffer urlPath := "/api/2.0/workspace-files/" + url.PathEscape(strings.TrimLeft(absPath, "/")) - err = s.apiClient.Do(ctx, http.MethodGet, urlPath, s.orgIDHeaders(), nil, nil, &buf) + err = s.apiClient.Do(ctx, http.MethodGet, urlPath, s.workspaceIDHeaders(), nil, nil, &buf) if err != nil { return nil, err } From 38859c115adc64a3f35b9c0b9f730e1c4d9683ea Mon Sep 17 00:00:00 2001 From: Divyansh Vijayvergia <171924202+Divyansh-db@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:56:54 +0200 Subject: [PATCH 4/4] Accept ?w= URL query parameter alongside ?o= (#5373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The Databricks UI is migrating from `?o=` to `?w=` as the SPOG URL query parameter, matching the new workspace addressing header. This PR extends the CLI's URL parsers to recognize `?w=` in addition to the existing `?o=` and `?workspace_id=` spellings. Pure addition; no existing URL changes meaning. Stacked on top of #5368 (which renames the request header to `X-Databricks-Workspace-Id`); the two together complete the input + wire side of the URL/header migration for the core CLI. ## Affected entry points - `databricks api ` — workspace ID is extracted from the path and sent as the routing header on the call. - `databricks auth login --host "https://...?w=..."` — workspace ID is extracted from the host URL and persisted to the profile. - `workspace.host` in `databricks.yml` — uses the same shared parser (`libs/auth.ExtractHostQueryParams`). ## Precedence When more than one spelling appears on a single URL, **`?o=` > `?w=` > `?workspace_id=`**. The `o`-first rule preserves the resolution of any URL already pasted from older UI builds, shell history, or committed `databricks.yml` files. ## Rename `extractOrgIDFromQuery` (in `cmd/api/api.go`) → `extractWorkspaceIDFromQuery`. The helper now returns the value under any of the recognized parameter names, so the old name became misleading. Unexported, single call site; updated atomically. ## Files - `libs/auth/hostparams.go` — adds the `q.Get("w")` branch in `ExtractHostQueryParams`; comment refreshed to document the three accepted forms and precedence. - `cmd/api/api.go` — adds `workspaceIDQueryParam = "w"` const; renames extractor and updates its body to check `o` then `w`. - `cmd/auth/login.go` — help text updated to recommend `?w=` and note that `?o=` / `?workspace_id=` are still accepted. - `libs/auth/hostparams_test.go` — new cases for `?w=`, precedence, and non-numeric rejection. - `cmd/api/api_test.go` — new cases in `TestExtractWorkspaceIDFromQuery` (renamed) and `TestResolveOrgID` covering `?w=` and the `o`-wins-over-`w` precedence. - `acceptance/cmd/api/workspace-id-from-w-query/` — new acceptance test mirroring `workspace-id-from-query/` but exercising the `?w=` path. The original `?o=` test stays unchanged as a regression check. ## Test plan - [x] \`go test ./libs/auth/... ./cmd/api/... ./cmd/auth/... -count=1\` — green - [x] \`go test ./acceptance -run 'TestAccept/cmd/api|TestAccept/cmd/auth|TestAccept/auth'\` — green - [x] \`./task lint-q\` — 0 issues - [x] \`./task fmt\` — no changes --- .../workspace-id-from-w-query/out.test.toml | 3 ++ .../api/workspace-id-from-w-query/output.txt | 21 +++++++++++ .../cmd/api/workspace-id-from-w-query/script | 2 + cmd/api/api.go | 37 ++++++++++++------- cmd/api/api_test.go | 30 ++++++++++++++- cmd/auth/login.go | 6 ++- libs/auth/hostparams.go | 20 +++++++--- libs/auth/hostparams_test.go | 29 ++++++++++++++- 8 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/out.test.toml create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/output.txt create mode 100644 acceptance/cmd/api/workspace-id-from-w-query/script diff --git a/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml b/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-from-w-query/output.txt b/acceptance/cmd/api/workspace-id-from-w-query/output.txt new file mode 100644 index 00000000000..34d0d3c847a --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/output.txt @@ -0,0 +1,21 @@ +{} + +>>> print_requests.py --get //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Workspace-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list", + "q": { + "w": "999" + } +} diff --git a/acceptance/cmd/api/workspace-id-from-w-query/script b/acceptance/cmd/api/workspace-id-from-w-query/script new file mode 100644 index 00000000000..f60139e4b7c --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-w-query/script @@ -0,0 +1,2 @@ +MSYS_NO_PATHCONV=1 $CLI api get "/api/2.0/clusters/list?w=999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999" diff --git a/cmd/api/api.go b/cmd/api/api.go index 044584c1220..59528527b53 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -27,12 +27,18 @@ const ( // header for rollback safety. workspaceIDHeader = "X-Databricks-Workspace-Id" - // orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used - // by the Databricks UI: "?o=" identifies the workspace a URL - // targets. When present on the path, we treat it as a per-call override - // for the workspace routing identifier so that pasted SPOG URLs route - // correctly without requiring --workspace-id. - orgIDQueryParam = "o" + // orgIDQueryParam and workspaceIDQueryParam are the SPOG + // (single-page-of-glass) URL convention used by the Databricks UI: + // "?o=" or "?w=" identifies the workspace a + // URL targets. When present on the path, we treat it as a per-call + // override for the workspace routing identifier so that pasted SPOG URLs + // route correctly without requiring --workspace-id. "w" is the new + // spelling that matches the X-Databricks-Workspace-Id header; "o" stays + // accepted for URLs already pasted from older UI builds, shell history, + // or committed databricks.yml files. "o" takes precedence when both are + // present to preserve the meaning of existing URLs. + orgIDQueryParam = "o" + workspaceIDQueryParam = "w" ) // accountSegmentRe matches a non-empty segment immediately after "accounts/", @@ -165,14 +171,19 @@ func hasAccountSegment(rawPath string) (bool, error) { return accountSegmentRe.MatchString(p), nil } -// extractOrgIDFromQuery returns the value of the "o" query parameter on path -// (the SPOG URL convention), or "" if absent or empty. -func extractOrgIDFromQuery(rawPath string) (string, error) { +// extractWorkspaceIDFromQuery returns the workspace ID encoded in the path's +// query string (the SPOG URL convention). It checks "o" first, then "w"; +// returns "" if neither is present or non-empty. +func extractWorkspaceIDFromQuery(rawPath string) (string, error) { u, err := url.Parse(rawPath) if err != nil { return "", fmt.Errorf("parse path: %w", err) } - return u.Query().Get(orgIDQueryParam), nil + q := u.Query() + if v := q.Get(orgIDQueryParam); v != "" { + return v, nil + } + return q.Get(workspaceIDQueryParam), nil } // resolveOrgID picks the value (if any) for the workspace routing identifier @@ -197,12 +208,12 @@ func resolveOrgID( } return workspaceIDFlag, nil } - orgIDFromQuery, err := extractOrgIDFromQuery(path) + workspaceIDFromQuery, err := extractWorkspaceIDFromQuery(path) if err != nil { return "", err } - if orgIDFromQuery != "" { - return orgIDFromQuery, nil + if workspaceIDFromQuery != "" { + return workspaceIDFromQuery, nil } isAccount, err := hasAccountSegment(path) if err != nil { diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index 69cd28fe5fe..0a50b1d842c 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -46,7 +46,7 @@ func TestHasAccountSegment(t *testing.T) { } } -func TestExtractOrgIDFromQuery(t *testing.T) { +func TestExtractWorkspaceIDFromQuery(t *testing.T) { cases := []struct { name string path string @@ -60,10 +60,15 @@ func TestExtractOrgIDFromQuery(t *testing.T) { {"unrelated o-prefixed param ignored", "/api/2.0/clusters/list?other=1", ""}, {"absolute URL", "https://example.com/api/2.0/clusters/list?o=42", "42"}, {"first value wins on duplicate", "/api/2.0/clusters/list?o=1&o=2", "1"}, + {"w param present", "/api/2.2/jobs/list?w=7474644166319138", "7474644166319138"}, + {"w param empty", "/api/2.0/clusters/list?w=", ""}, + {"w among other params", "/api/2.0/clusters/list?foo=bar&w=123", "123"}, + {"o wins over w when both present", "/api/2.0/clusters/list?o=111&w=222", "111"}, + {"w used when o is empty", "/api/2.0/clusters/list?o=&w=222", "222"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - got, err := extractOrgIDFromQuery(c.path) + got, err := extractWorkspaceIDFromQuery(c.path) require.NoError(t, err) assert.Equal(t, c.want, got) }) @@ -76,6 +81,7 @@ func TestResolveOrgID(t *testing.T) { accountPath = "/api/2.0/accounts/abc-123/network-policies" proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets" spogPath = "/api/2.2/jobs/list?o=7474644166319138" + spogPathW = "/api/2.2/jobs/list?w=7474644166319138" spogAccountPath = "/api/2.0/accounts/abc-123/network-policies?o=7474644166319138" spogWorkspaceID = "7474644166319138" resolvedWSID = "900800700600" @@ -189,6 +195,26 @@ func TestResolveOrgID(t *testing.T) { path: spogAccountPath, want: spogWorkspaceID, }, + { + name: "?w= sets identifier when no flag and no profile WorkspaceID", + cfgWorkspaceID: "", + path: spogPathW, + want: spogWorkspaceID, + }, + { + name: "?w= overrides profile WorkspaceID", + cfgWorkspaceID: resolvedWSID, + path: spogPathW, + want: spogWorkspaceID, + }, + { + name: "--workspace-id wins over ?w=", + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: spogPathW, + want: flagWSID, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 2e8cce02404..640ee7f16ba 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -118,9 +118,11 @@ use the flags directly to specify both. The host URL may include query parameters to set the workspace and account ID: - databricks auth login --host "https://?o=&account_id=" + databricks auth login --host "https://?w=&account_id=" -Note: URLs containing "?" must be quoted to prevent shell interpretation. +The workspace ID may be passed as ?w= (preferred), ?o= (legacy), or +?workspace_id=. Note: URLs containing "?" must be quoted to prevent shell +interpretation. If a profile with the given name already exists, it is updated. Otherwise a new profile is created. diff --git a/libs/auth/hostparams.go b/libs/auth/hostparams.go index f363cfa5a35..103d2834dcb 100644 --- a/libs/auth/hostparams.go +++ b/libs/auth/hostparams.go @@ -11,8 +11,10 @@ type HostParams struct { // Host is the URL with query parameters stripped. Host string - // WorkspaceID extracted from ?o= or ?workspace_id=. - // Empty if not present or not numeric. + // WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=. + // Empty if not present. ?o= and ?workspace_id= are legacy spellings that + // remain numeric-only; ?w= is the new spelling and is passed through + // unchanged so non-numeric connection-style identifiers reach the server. WorkspaceID string // AccountID extracted from ?a= or ?account_id=. @@ -21,9 +23,15 @@ type HostParams struct { } // ExtractHostQueryParams parses recognized query parameters from a host URL. -// Recognized parameters: o (workspace_id), workspace_id, a (account_id), account_id. -// Workspace IDs must be numeric; non-numeric values are ignored. -// The returned Host has all query parameters and fragments stripped. +// Recognized parameters: o (workspace_id), w (workspace_id), workspace_id, +// a (account_id), account_id. The "w" spelling matches the new +// X-Databricks-Workspace-Id routing header and accepts any non-empty value +// (including non-numeric connection-style identifiers). The legacy "o" and +// "workspace_id" spellings remain numeric-only — they predate the broader +// identifier shapes and historical URLs carrying those forms are always +// numeric. When more than one spelling is present, "o" wins to preserve the +// meaning of existing URLs. The returned Host has all query parameters and +// fragments stripped. func ExtractHostQueryParams(host string) HostParams { u, err := url.Parse(host) if err != nil || u.RawQuery == "" { @@ -37,6 +45,8 @@ func ExtractHostQueryParams(host string) HostParams { if _, err := strconv.ParseInt(v, 10, 64); err == nil { workspaceID = v } + } else if v := q.Get("w"); v != "" { + workspaceID = v } else if v := q.Get("workspace_id"); v != "" { if _, err := strconv.ParseInt(v, 10, 64); err == nil { workspaceID = v diff --git a/libs/auth/hostparams_test.go b/libs/auth/hostparams_test.go index 900c69fb92d..5fe43a3045a 100644 --- a/libs/auth/hostparams_test.go +++ b/libs/auth/hostparams_test.go @@ -27,26 +27,51 @@ func TestExtractHostQueryParams(t *testing.T) { host: "https://spog.example.com/?account_id=abc", want: HostParams{Host: "https://spog.example.com", AccountID: "abc"}, }, + { + name: "extract workspace_id from ?w=", + host: "https://spog.example.com/?w=12345", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "12345"}, + }, { name: "extract workspace_id from ?workspace_id=", host: "https://spog.example.com/?workspace_id=99999", want: HostParams{Host: "https://spog.example.com", WorkspaceID: "99999"}, }, + { + name: "?o= wins over ?w= when both present", + host: "https://spog.example.com/?o=11111&w=22222", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"}, + }, + { + name: "?w= wins over ?workspace_id= when both present", + host: "https://spog.example.com/?w=11111&workspace_id=22222", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"}, + }, { name: "no query params leaves host unchanged", host: "https://spog.example.com", want: HostParams{Host: "https://spog.example.com"}, }, { - name: "non-numeric ?o= is skipped", + name: "non-numeric ?o= is skipped (legacy spelling stays numeric-only)", host: "https://spog.example.com/?o=abc", want: HostParams{Host: "https://spog.example.com"}, }, { - name: "non-numeric ?workspace_id= is skipped", + name: "non-numeric ?w= is passed through", + host: "https://spog.example.com/?w=abc", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"}, + }, + { + name: "non-numeric ?workspace_id= is skipped (legacy spelling stays numeric-only)", host: "https://spog.example.com/?workspace_id=abc", want: HostParams{Host: "https://spog.example.com"}, }, + { + name: "connection-id-style ?w= value passed through", + host: "https://spog.example.com/?w=123e4567-e89b-12d3-a456-426614174000", + want: HostParams{Host: "https://spog.example.com", WorkspaceID: "123e4567-e89b-12d3-a456-426614174000"}, + }, { name: "invalid URL is left unchanged", host: "not a valid url ://???",