Skip to content

Commit 146a88a

Browse files
committed
Accept ?w= URL query parameter alongside ?o=
The Databricks UI is migrating from ?o=<workspace-id> to ?w=<workspace-id> as the SPOG URL query parameter, matching the new workspace addressing header. Extend 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. This affects: - databricks api <verb> <path?w=...> — workspace ID extracted from the path and sent as the routing header on the call. - databricks auth login --host "https://...?w=..." — workspace ID extracted from the host URL and persisted to the profile. - workspace.host in databricks.yml — same parser is reused. When more than one spelling is present on a single URL, ?o= takes precedence over ?w=, which takes precedence over ?workspace_id=. The "o" precedence preserves the meaning of any URL already pasted from older UI builds, shell history, or committed databricks.yml files. The cmd/api helper extractOrgIDFromQuery is renamed to extractWorkspaceIDFromQuery to reflect that it now returns the value under multiple recognized parameter names.
1 parent c579a1c commit 146a88a

8 files changed

Lines changed: 115 additions & 21 deletions

File tree

acceptance/cmd/api/workspace-id-from-w-query/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{}
2+
3+
>>> print_requests.py --get //api/2.0/clusters/list
4+
{
5+
"headers": {
6+
"Authorization": [
7+
"Bearer [DATABRICKS_TOKEN]"
8+
],
9+
"User-Agent": [
10+
"cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat"
11+
],
12+
"X-Databricks-Workspace-Id": [
13+
"999"
14+
]
15+
},
16+
"method": "GET",
17+
"path": "/api/2.0/clusters/list",
18+
"q": {
19+
"w": "999"
20+
}
21+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MSYS_NO_PATHCONV=1 $CLI api get "/api/2.0/clusters/list?w=999"
2+
trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Workspace-Id" "999"

cmd/api/api.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ const (
2727
// header for rollback safety.
2828
workspaceIDHeader = "X-Databricks-Workspace-Id"
2929

30-
// orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used
31-
// by the Databricks UI: "?o=<workspace-id>" identifies the workspace a URL
32-
// targets. When present on the path, we treat it as a per-call override
33-
// for the workspace routing identifier so that pasted SPOG URLs route
34-
// correctly without requiring --workspace-id.
35-
orgIDQueryParam = "o"
30+
// orgIDQueryParam and workspaceIDQueryParam are the SPOG
31+
// (single-page-of-glass) URL convention used by the Databricks UI:
32+
// "?o=<workspace-id>" or "?w=<workspace-id>" identifies the workspace a
33+
// URL targets. When present on the path, we treat it as a per-call
34+
// override for the workspace routing identifier so that pasted SPOG URLs
35+
// route correctly without requiring --workspace-id. "w" is the new
36+
// spelling that matches the X-Databricks-Workspace-Id header; "o" stays
37+
// accepted for URLs already pasted from older UI builds, shell history,
38+
// or committed databricks.yml files. "o" takes precedence when both are
39+
// present to preserve the meaning of existing URLs.
40+
orgIDQueryParam = "o"
41+
workspaceIDQueryParam = "w"
3642
)
3743

3844
// accountSegmentRe matches a non-empty segment immediately after "accounts/",
@@ -165,14 +171,19 @@ func hasAccountSegment(rawPath string) (bool, error) {
165171
return accountSegmentRe.MatchString(p), nil
166172
}
167173

168-
// extractOrgIDFromQuery returns the value of the "o" query parameter on path
169-
// (the SPOG URL convention), or "" if absent or empty.
170-
func extractOrgIDFromQuery(rawPath string) (string, error) {
174+
// extractWorkspaceIDFromQuery returns the workspace ID encoded in the path's
175+
// query string (the SPOG URL convention). It checks "o" first, then "w";
176+
// returns "" if neither is present or non-empty.
177+
func extractWorkspaceIDFromQuery(rawPath string) (string, error) {
171178
u, err := url.Parse(rawPath)
172179
if err != nil {
173180
return "", fmt.Errorf("parse path: %w", err)
174181
}
175-
return u.Query().Get(orgIDQueryParam), nil
182+
q := u.Query()
183+
if v := q.Get(orgIDQueryParam); v != "" {
184+
return v, nil
185+
}
186+
return q.Get(workspaceIDQueryParam), nil
176187
}
177188

178189
// resolveOrgID picks the value (if any) for the workspace routing identifier
@@ -197,12 +208,12 @@ func resolveOrgID(
197208
}
198209
return workspaceIDFlag, nil
199210
}
200-
orgIDFromQuery, err := extractOrgIDFromQuery(path)
211+
workspaceIDFromQuery, err := extractWorkspaceIDFromQuery(path)
201212
if err != nil {
202213
return "", err
203214
}
204-
if orgIDFromQuery != "" {
205-
return orgIDFromQuery, nil
215+
if workspaceIDFromQuery != "" {
216+
return workspaceIDFromQuery, nil
206217
}
207218
isAccount, err := hasAccountSegment(path)
208219
if err != nil {

cmd/api/api_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestHasAccountSegment(t *testing.T) {
4646
}
4747
}
4848

49-
func TestExtractOrgIDFromQuery(t *testing.T) {
49+
func TestExtractWorkspaceIDFromQuery(t *testing.T) {
5050
cases := []struct {
5151
name string
5252
path string
@@ -60,10 +60,15 @@ func TestExtractOrgIDFromQuery(t *testing.T) {
6060
{"unrelated o-prefixed param ignored", "/api/2.0/clusters/list?other=1", ""},
6161
{"absolute URL", "https://example.com/api/2.0/clusters/list?o=42", "42"},
6262
{"first value wins on duplicate", "/api/2.0/clusters/list?o=1&o=2", "1"},
63+
{"w param present", "/api/2.2/jobs/list?w=7474644166319138", "7474644166319138"},
64+
{"w param empty", "/api/2.0/clusters/list?w=", ""},
65+
{"w among other params", "/api/2.0/clusters/list?foo=bar&w=123", "123"},
66+
{"o wins over w when both present", "/api/2.0/clusters/list?o=111&w=222", "111"},
67+
{"w used when o is empty", "/api/2.0/clusters/list?o=&w=222", "222"},
6368
}
6469
for _, c := range cases {
6570
t.Run(c.name, func(t *testing.T) {
66-
got, err := extractOrgIDFromQuery(c.path)
71+
got, err := extractWorkspaceIDFromQuery(c.path)
6772
require.NoError(t, err)
6873
assert.Equal(t, c.want, got)
6974
})
@@ -76,6 +81,7 @@ func TestResolveOrgID(t *testing.T) {
7681
accountPath = "/api/2.0/accounts/abc-123/network-policies"
7782
proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets"
7883
spogPath = "/api/2.2/jobs/list?o=7474644166319138"
84+
spogPathW = "/api/2.2/jobs/list?w=7474644166319138"
7985
spogAccountPath = "/api/2.0/accounts/abc-123/network-policies?o=7474644166319138"
8086
spogWorkspaceID = "7474644166319138"
8187
resolvedWSID = "900800700600"
@@ -189,6 +195,26 @@ func TestResolveOrgID(t *testing.T) {
189195
path: spogAccountPath,
190196
want: spogWorkspaceID,
191197
},
198+
{
199+
name: "?w=<id> sets identifier when no flag and no profile WorkspaceID",
200+
cfgWorkspaceID: "",
201+
path: spogPathW,
202+
want: spogWorkspaceID,
203+
},
204+
{
205+
name: "?w=<id> overrides profile WorkspaceID",
206+
cfgWorkspaceID: resolvedWSID,
207+
path: spogPathW,
208+
want: spogWorkspaceID,
209+
},
210+
{
211+
name: "--workspace-id wins over ?w=",
212+
workspaceIDFlag: flagWSID,
213+
flagSet: true,
214+
cfgWorkspaceID: resolvedWSID,
215+
path: spogPathW,
216+
want: flagWSID,
217+
},
192218
}
193219
for _, c := range cases {
194220
t.Run(c.name, func(t *testing.T) {

cmd/auth/login.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,11 @@ use the flags directly to specify both.
118118
119119
The host URL may include query parameters to set the workspace and account ID:
120120
121-
databricks auth login --host "https://<host>?o=<workspace_id>&account_id=<id>"
121+
databricks auth login --host "https://<host>?w=<workspace_id>&account_id=<id>"
122122
123-
Note: URLs containing "?" must be quoted to prevent shell interpretation.
123+
The workspace ID may be passed as ?w= (preferred), ?o= (legacy), or
124+
?workspace_id=. Note: URLs containing "?" must be quoted to prevent shell
125+
interpretation.
124126
125127
If a profile with the given name already exists, it is updated. Otherwise
126128
a new profile is created.

libs/auth/hostparams.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type HostParams struct {
1111
// Host is the URL with query parameters stripped.
1212
Host string
1313

14-
// WorkspaceID extracted from ?o= or ?workspace_id=.
14+
// WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=.
1515
// Empty if not present or not numeric.
1616
WorkspaceID string
1717

@@ -21,9 +21,14 @@ type HostParams struct {
2121
}
2222

2323
// ExtractHostQueryParams parses recognized query parameters from a host URL.
24-
// Recognized parameters: o (workspace_id), workspace_id, a (account_id), account_id.
25-
// Workspace IDs must be numeric; non-numeric values are ignored.
26-
// The returned Host has all query parameters and fragments stripped.
24+
// Recognized parameters: o (workspace_id), w (workspace_id), workspace_id,
25+
// a (account_id), account_id. The "w" spelling matches the new
26+
// X-Databricks-Workspace-Id routing header; "o" is the legacy SPOG form and
27+
// stays accepted so URLs already in databricks.yml / shell history keep
28+
// working. When more than one is present, "o" wins to preserve the meaning
29+
// of existing URLs. Workspace IDs must be numeric; non-numeric values are
30+
// ignored. The returned Host has all query parameters and fragments
31+
// stripped.
2732
func ExtractHostQueryParams(host string) HostParams {
2833
u, err := url.Parse(host)
2934
if err != nil || u.RawQuery == "" {
@@ -37,6 +42,10 @@ func ExtractHostQueryParams(host string) HostParams {
3742
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
3843
workspaceID = v
3944
}
45+
} else if v := q.Get("w"); v != "" {
46+
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
47+
workspaceID = v
48+
}
4049
} else if v := q.Get("workspace_id"); v != "" {
4150
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
4251
workspaceID = v

libs/auth/hostparams_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,26 @@ func TestExtractHostQueryParams(t *testing.T) {
2727
host: "https://spog.example.com/?account_id=abc",
2828
want: HostParams{Host: "https://spog.example.com", AccountID: "abc"},
2929
},
30+
{
31+
name: "extract workspace_id from ?w=",
32+
host: "https://spog.example.com/?w=12345",
33+
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "12345"},
34+
},
3035
{
3136
name: "extract workspace_id from ?workspace_id=",
3237
host: "https://spog.example.com/?workspace_id=99999",
3338
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "99999"},
3439
},
40+
{
41+
name: "?o= wins over ?w= when both present",
42+
host: "https://spog.example.com/?o=11111&w=22222",
43+
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"},
44+
},
45+
{
46+
name: "?w= wins over ?workspace_id= when both present",
47+
host: "https://spog.example.com/?w=11111&workspace_id=22222",
48+
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "11111"},
49+
},
3550
{
3651
name: "no query params leaves host unchanged",
3752
host: "https://spog.example.com",
@@ -42,6 +57,11 @@ func TestExtractHostQueryParams(t *testing.T) {
4257
host: "https://spog.example.com/?o=abc",
4358
want: HostParams{Host: "https://spog.example.com"},
4459
},
60+
{
61+
name: "non-numeric ?w= is skipped",
62+
host: "https://spog.example.com/?w=abc",
63+
want: HostParams{Host: "https://spog.example.com"},
64+
},
4565
{
4666
name: "non-numeric ?workspace_id= is skipped",
4767
host: "https://spog.example.com/?workspace_id=abc",

0 commit comments

Comments
 (0)