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-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/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..01267fa3a72 100644 --- a/acceptance/telemetry/success/out.requests.txt +++ b/acceptance/telemetry/success/out.requests.txt @@ -15,7 +15,7 @@ "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" ], - "X-Databricks-Org-Id": [ + "X-Databricks-Workspace-Id": [ "[NUMID]" ] }, 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/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 } diff --git a/cmd/api/api.go b/cmd/api/api.go index a832f018c1a..59528527b53 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -20,17 +20,25 @@ 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" - - // 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" + // 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 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/", @@ -114,7 +122,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 @@ -163,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 @@ -195,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 ://???", diff --git a/libs/filer/files_client.go b/libs/filer/files_client.go index 6abfa1e66c3..b09d10de7af 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 3663473e92a..d49b10b8ace 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 f9df3a97f7b..7cda184bd04 100644 --- a/libs/telemetry/logger.go +++ b/libs/telemetry/logger.go @@ -170,23 +170,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