Skip to content

Commit 38859c1

Browse files
authored
Accept ?w= URL query parameter alongside ?o= (#5373)
## Summary 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. 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 <verb> <path?w=...>` — 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
1 parent c579a1c commit 38859c1

8 files changed

Lines changed: 124 additions & 24 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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ type HostParams struct {
1111
// Host is the URL with query parameters stripped.
1212
Host string
1313

14-
// WorkspaceID extracted from ?o= or ?workspace_id=.
15-
// Empty if not present or not numeric.
14+
// WorkspaceID extracted from ?o=, ?w=, or ?workspace_id=.
15+
// Empty if not present. ?o= and ?workspace_id= are legacy spellings that
16+
// remain numeric-only; ?w= is the new spelling and is passed through
17+
// unchanged so non-numeric connection-style identifiers reach the server.
1618
WorkspaceID string
1719

1820
// AccountID extracted from ?a= or ?account_id=.
@@ -21,9 +23,15 @@ type HostParams struct {
2123
}
2224

2325
// 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.
26+
// Recognized parameters: o (workspace_id), w (workspace_id), workspace_id,
27+
// a (account_id), account_id. The "w" spelling matches the new
28+
// X-Databricks-Workspace-Id routing header and accepts any non-empty value
29+
// (including non-numeric connection-style identifiers). The legacy "o" and
30+
// "workspace_id" spellings remain numeric-only — they predate the broader
31+
// identifier shapes and historical URLs carrying those forms are always
32+
// numeric. When more than one spelling is present, "o" wins to preserve the
33+
// meaning of existing URLs. The returned Host has all query parameters and
34+
// fragments stripped.
2735
func ExtractHostQueryParams(host string) HostParams {
2836
u, err := url.Parse(host)
2937
if err != nil || u.RawQuery == "" {
@@ -37,6 +45,8 @@ func ExtractHostQueryParams(host string) HostParams {
3745
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
3846
workspaceID = v
3947
}
48+
} else if v := q.Get("w"); v != "" {
49+
workspaceID = v
4050
} else if v := q.Get("workspace_id"); v != "" {
4151
if _, err := strconv.ParseInt(v, 10, 64); err == nil {
4252
workspaceID = v

libs/auth/hostparams_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,51 @@ 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",
3853
want: HostParams{Host: "https://spog.example.com"},
3954
},
4055
{
41-
name: "non-numeric ?o= is skipped",
56+
name: "non-numeric ?o= is skipped (legacy spelling stays numeric-only)",
4257
host: "https://spog.example.com/?o=abc",
4358
want: HostParams{Host: "https://spog.example.com"},
4459
},
4560
{
46-
name: "non-numeric ?workspace_id= is skipped",
61+
name: "non-numeric ?w= is passed through",
62+
host: "https://spog.example.com/?w=abc",
63+
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "abc"},
64+
},
65+
{
66+
name: "non-numeric ?workspace_id= is skipped (legacy spelling stays numeric-only)",
4767
host: "https://spog.example.com/?workspace_id=abc",
4868
want: HostParams{Host: "https://spog.example.com"},
4969
},
70+
{
71+
name: "connection-id-style ?w= value passed through",
72+
host: "https://spog.example.com/?w=123e4567-e89b-12d3-a456-426614174000",
73+
want: HostParams{Host: "https://spog.example.com", WorkspaceID: "123e4567-e89b-12d3-a456-426614174000"},
74+
},
5075
{
5176
name: "invalid URL is left unchanged",
5277
host: "not a valid url ://???",

0 commit comments

Comments
 (0)