Skip to content

Commit f171c1f

Browse files
authored
SPOG: short-circuit CurrentWorkspaceID and add org-id header to SharesAPI (#1635)
## Changes Two SPOG fixes for hand-written SDK extension methods that bypass the generated code's per-call `X-Databricks-Org-Id` header logic. ### 1. Short-circuit `WorkspaceClient.CurrentWorkspaceID()` Return `Config.WorkspaceID` directly when it is non-empty, instead of making a `GET` to `/api/2.0/preview/scim/v2/Me` purely to read the workspace id from the `X-Databricks-Org-Id` response header. In the CLI bundle flow, `Config.WorkspaceID` is already populated before this call runs, from any of: - the matched profile in `.databrickscfg` (`workspace_id = ...`) - the `?o=<id>` query parameter extracted from the bundle host - the `DATABRICKS_WORKSPACE_ID` env var - host metadata at `/.well-known/databricks-config` Calling `Me()` to re-learn that value is a wasted round-trip, and on SPOG hosts it also breaks `databricks bundle open`: ``` Error: Unable to load OAuth Config (400 UNKNOWN) Endpoint: GET https://<spog-host>/api/2.0/preview/scim/v2/Me HTTP Status: 400 Bad Request ``` The previous implementation used `httpclient.WithResponseHeader` to read `X-Databricks-Org-Id` from the response, but did not set it on the request. SPOG's proxy rejects un-routed requests with the exact 400 above. #### Why the short-circuit is sufficient On SPOG, `WorkspaceID` is the routing key. To call `Me()` the request must carry `X-Databricks-Org-Id`. - If `Config.WorkspaceID` is set, return it without any API call. - If `Config.WorkspaceID` is empty, we have no value to put in the header, so `Me()` on SPOG would fail regardless. A header-setting patch would add no reachable value. On non-SPOG hosts the fallback API call still works without the header, preserving pre-existing behavior. ### 2. Add `X-Databricks-Org-Id` to `SharesAPI.internalList()` `service/sharing/ext_api.go` `SharesAPI.internalList()` is a hand-written extension that calls `a.client.Do()` directly and was missing the `X-Databricks-Org-Id` request header. On SPOG hosts this causes `shares.List(Paginated|All)` to fail with the same `Unable to load OAuth Config` error. Matches the fix in #1634 for `service/workspace/ext_utilities.go` `Upload()` / `Download()`. Pointed out by Hector Castejon Diaz. ## Tests ### Unit tests Added `workspace_functions_test.go`: - `TestCurrentWorkspaceIDShortCircuitsWhenConfigHasWorkspaceID`: fails the test if `Me()` is called when `Config.WorkspaceID` is set, and asserts the returned value matches the configured id. - `TestCurrentWorkspaceIDFallsBackToAPIWhenConfigMissingWorkspaceID`: asserts the pre-existing API fallback still works and parses the `X-Databricks-Org-Id` response header correctly. No dedicated unit test added for the sharing header change; matches #1634 which applied the same pattern without one. `make fmt test lint` clean on the full SDK (1344 tests pass, 103 skipped). ### Empirical SPOG verification Same bearer token, same endpoint, toggling only the request header: | Request | Response | | ------------------------------------------ | -------------------------------------- | | `GET /Me` (no `X-Databricks-Org-Id`) | HTTP 400 `Unable to load OAuth Config` | | `GET /Me` with `X-Databricks-Org-Id: <id>` | HTTP 200 with valid user JSON | Confirms the missing request header is the cause. ### End-to-end with the CLI Built the CLI against this branch via `go mod edit -replace` and ran on a real SPOG workspace (`db-deco-test-chrisst`, host `https://db-deco-test.databricks.com`): - `databricks bundle validate -t dev`: OK - `databricks bundle deploy -t dev`: OK - `databricks bundle run hello_world -t dev`: OK, job TERMINATED SUCCESS - `databricks bundle open hello_world -t dev`: OK, opens the job URL. Previously failed with `Unable to load OAuth Config (400 UNKNOWN)`. Signed-off-by: simon <simon.faltum@databricks.com> --------- Signed-off-by: simon <simon.faltum@databricks.com>
1 parent b3cd610 commit f171c1f

4 files changed

Lines changed: 77 additions & 0 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
### Bug Fixes
1010

1111
* Add `X-Databricks-Org-Id` header to `Workspace.Download()` and `Workspace.Upload()` for SPOG host compatibility.
12+
* `WorkspaceClient.CurrentWorkspaceID()` now returns `Config.WorkspaceID` directly when set, instead of calling `/api/2.0/preview/scim/v2/Me`. This removes an API round-trip on every call where the workspace ID is already known (profile, `?o=` query param, env var, or host metadata) and fixes a failure on SPOG hosts where the unauthenticated probe request was rejected with `Unable to load OAuth Config`.
13+
* Add `X-Databricks-Org-Id` header to `SharesAPI.internalList()` (`service/sharing/ext_api.go`) for SPOG host compatibility. Same class of bug as #1634 - a hand-written extension method was calling `a.client.Do()` without the header that the generated code paths set per-call.
1214

1315
### Documentation
1416

service/sharing/ext_api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ func (a *SharesAPI) internalList(ctx context.Context, request ListSharesRequest)
7171
queryParams := make(map[string]any)
7272
headers := make(map[string]string)
7373
headers["Accept"] = "application/json"
74+
cfg := a.client.Config
75+
if cfg.WorkspaceID != "" {
76+
headers["X-Databricks-Org-Id"] = cfg.WorkspaceID
77+
}
7478
err := a.client.Do(ctx, http.MethodGet, path, headers, queryParams, request, &listSharesResponse)
7579
return &listSharesResponse, err
7680
}

workspace_functions.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import (
99

1010
// CurrentWorkspaceID returns the workspace ID of the workspace that this client is
1111
// connected to.
12+
//
13+
// If Config.WorkspaceID is already set (from the databrickscfg profile, the
14+
// DATABRICKS_WORKSPACE_ID env var, host metadata, or a ?o= query param extracted
15+
// by the caller), it is returned without an API round-trip. Otherwise the ID is
16+
// fetched from the X-Databricks-Org-Id response header on /api/2.0/preview/scim/v2/Me.
1217
func (w *WorkspaceClient) CurrentWorkspaceID(ctx context.Context) (int64, error) {
18+
if w.Config != nil && w.Config.WorkspaceID != "" {
19+
return strconv.ParseInt(w.Config.WorkspaceID, 10, 64)
20+
}
1321
var workspaceIdStr string
1422
err := w.apiClient.Do(ctx, "GET", "/api/2.0/preview/scim/v2/Me", httpclient.WithResponseHeader("X-Databricks-Org-Id", &workspaceIdStr))
1523
if err != nil {

workspace_functions_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package databricks
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestCurrentWorkspaceIDShortCircuitsWhenConfigHasWorkspaceID(t *testing.T) {
13+
// When Config.WorkspaceID is already populated (from profile, ?o= query
14+
// param, env var, or host metadata), CurrentWorkspaceID returns it without
15+
// hitting the API. This avoids a round-trip and sidesteps SPOG's routing
16+
// requirement that requests to /api/2.0/preview/scim/v2/Me carry an
17+
// X-Databricks-Org-Id header.
18+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
if r.URL.Path == "/api/2.0/preview/scim/v2/Me" {
20+
t.Errorf("Me() should not be called when Config.WorkspaceID is set")
21+
}
22+
http.NotFound(w, r)
23+
}))
24+
defer server.Close()
25+
26+
w, err := NewWorkspaceClient(&Config{
27+
Host: server.URL,
28+
Token: "token",
29+
WorkspaceID: "7474644166319138",
30+
})
31+
require.NoError(t, err)
32+
33+
got, err := w.CurrentWorkspaceID(t.Context())
34+
require.NoError(t, err)
35+
assert.Equal(t, int64(7474644166319138), got)
36+
}
37+
38+
func TestCurrentWorkspaceIDFallsBackToAPIWhenConfigMissingWorkspaceID(t *testing.T) {
39+
var meCalls int
40+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
41+
if r.URL.Path == "/api/2.0/preview/scim/v2/Me" {
42+
meCalls++
43+
w.Header().Set("X-Databricks-Org-Id", "7474644166319138")
44+
w.Write([]byte(`{}`))
45+
return
46+
}
47+
// Other bootstrap calls (e.g. /.well-known/databricks-config) are not
48+
// relevant to this test; respond with a benign 404.
49+
http.NotFound(w, r)
50+
}))
51+
defer server.Close()
52+
53+
w, err := NewWorkspaceClient(&Config{
54+
Host: server.URL,
55+
Token: "token",
56+
})
57+
require.NoError(t, err)
58+
59+
got, err := w.CurrentWorkspaceID(t.Context())
60+
require.NoError(t, err)
61+
assert.Equal(t, int64(7474644166319138), got)
62+
assert.Equal(t, 1, meCalls)
63+
}

0 commit comments

Comments
 (0)