Skip to content

Commit e2132d8

Browse files
committed
[WIP] Draft unified host changes
1 parent 7b9a86e commit e2132d8

File tree

8 files changed

+190
-20
lines changed

8 files changed

+190
-20
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Version changelog
22

3+
## Unreleased
4+
5+
### New Features and Improvements
6+
* Add support for unified hosts, i.e. hosts that support both workspace-level and account-level operations
7+
* Add `HostType` and `ConfigType` enums to `DatabricksConfig` for better host type management
8+
* Add `workspaceId` field to `DatabricksConfig` for workspace clients on unified hosts
9+
* Add `experimentalIsUnifiedHost` field to `DatabricksConfig` to mark unified hosts
10+
* Add `getHostType()` and `getConfigType()` methods to `DatabricksConfig`
11+
* Add X-Databricks-Org-Id header support for unified host workspace requests
12+
13+
### Deprecations
14+
* Deprecate `isAccountClient()` method in `DatabricksConfig`. Use `getHostType()` or `getConfigType()` instead.
15+
16+
### Internal Changes
17+
* Update OAuth endpoint discovery to support unified hosts
18+
* Update credential providers to use `getHostType()` instead of `isAccountClient()`
19+
320
## Release v0.68.0 (2025-10-30)
421

522
### Documentation

databricks-sdk-java/src/main/java/com/databricks/sdk/AccountClient.java

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

databricks-sdk-java/src/main/java/com/databricks/sdk/core/ApiClient.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public static class Builder {
4040
private Integer debugTruncateBytes;
4141
private HttpClient httpClient;
4242
private String accountId;
43+
private String workspaceId;
44+
private DatabricksConfig.HostType hostType;
4345
private RetryStrategyPicker retryStrategyPicker;
4446
private boolean isDebugHeaders;
4547

@@ -50,6 +52,8 @@ public Builder withDatabricksConfig(DatabricksConfig config) {
5052
this.httpClient = config.getHttpClient();
5153
this.debugTruncateBytes = config.getDebugTruncateBytes();
5254
this.accountId = config.getAccountId();
55+
this.workspaceId = config.getWorkspaceId();
56+
this.hostType = config.getHostType();
5357
this.isDebugHeaders = config.isDebugHeaders();
5458

5559
if (config.getDisableRetries()) {
@@ -112,6 +116,8 @@ public ApiClient build() {
112116
private final Function<Void, String> getHostFunc;
113117
private final Function<Void, String> getAuthTypeFunc;
114118
private final String accountId;
119+
private final String workspaceId;
120+
private final DatabricksConfig.HostType hostType;
115121
private final boolean isDebugHeaders;
116122
private static final String RETRY_AFTER_HEADER = "retry-after";
117123

@@ -141,6 +147,8 @@ private ApiClient(Builder builder) {
141147
this.getAuthTypeFunc = builder.getAuthTypeFunc != null ? builder.getAuthTypeFunc : v -> "";
142148
this.httpClient = builder.httpClient;
143149
this.accountId = builder.accountId;
150+
this.workspaceId = builder.workspaceId;
151+
this.hostType = builder.hostType;
144152
this.retryStrategyPicker =
145153
builder.retryStrategyPicker != null
146154
? builder.retryStrategyPicker
@@ -240,6 +248,16 @@ private Response executeInner(Request in, String path, RequestOptions options) {
240248
}
241249
in.withHeader("User-Agent", userAgent);
242250

251+
// Unified hosts use X-Databricks-Org-Id header to determine which workspace to route the
252+
// request to. The header must not be set for account-level API requests, otherwise the
253+
// request will fail. This relies on the assumption that workspaceId is only set for
254+
// workspace client configs.
255+
if (hostType == DatabricksConfig.HostType.UNIFIED_HOST
256+
&& workspaceId != null
257+
&& !workspaceId.isEmpty()) {
258+
in.withHeader("X-Databricks-Org-Id", workspaceId);
259+
}
260+
243261
options.applyOptions(in);
244262

245263
// Make the request, catching any exceptions, as we may want to retry.

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
2929
}
3030
List<String> cmd =
3131
new ArrayList<>(Arrays.asList(cliPath, "auth", "token", "--host", config.getHost()));
32-
if (config.isAccountClient()) {
32+
if (config.getHostType() != DatabricksConfig.HostType.WORKSPACE_HOST) {
3333
cmd.add("--account-id");
3434
cmd.add(config.getAccountId());
3535
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java

Lines changed: 145 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@
1919
import org.apache.http.HttpMessage;
2020

2121
public class DatabricksConfig {
22+
/** HostType represents the type of API the configured host supports. */
23+
public enum HostType {
24+
/** WorkspaceHost supports only workspace-level APIs. */
25+
WORKSPACE_HOST,
26+
/** AccountHost supports only account-level APIs. */
27+
ACCOUNT_HOST,
28+
/** UnifiedHost supports both workspace-level and account-level APIs. */
29+
UNIFIED_HOST
30+
}
31+
32+
/** ConfigType represents the type of API this config is valid for. */
33+
public enum ConfigType {
34+
/** WorkspaceConfig is valid for workspace-level API requests. */
35+
WORKSPACE_CONFIG,
36+
/** AccountConfig is valid for account-level API requests. */
37+
ACCOUNT_CONFIG,
38+
/**
39+
* InvalidConfig is returned when the config is not valid for either workspace-level or
40+
* account-level APIs.
41+
*/
42+
INVALID_CONFIG
43+
}
2244
private CredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
2345

2446
@ConfigAttribute(env = "DATABRICKS_HOST")
@@ -27,6 +49,14 @@ public class DatabricksConfig {
2749
@ConfigAttribute(env = "DATABRICKS_ACCOUNT_ID")
2850
private String accountId;
2951

52+
/** Databricks Workspace ID for Workspace clients when working with unified hosts. */
53+
@ConfigAttribute(env = "DATABRICKS_WORKSPACE_ID")
54+
private String workspaceId;
55+
56+
/** Marker for unified hosts. Will be redundant once we can recognize unified hosts by their hostname. */
57+
@ConfigAttribute(env = "DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST")
58+
private Boolean experimentalIsUnifiedHost;
59+
3060
@ConfigAttribute(env = "DATABRICKS_TOKEN", auth = "pat", sensitive = true)
3161
private String token;
3262

@@ -290,6 +320,24 @@ public DatabricksConfig setAccountId(String accountId) {
290320
return this;
291321
}
292322

323+
public String getWorkspaceId() {
324+
return workspaceId;
325+
}
326+
327+
public DatabricksConfig setWorkspaceId(String workspaceId) {
328+
this.workspaceId = workspaceId;
329+
return this;
330+
}
331+
332+
public boolean getExperimentalIsUnifiedHost() {
333+
return experimentalIsUnifiedHost != null && experimentalIsUnifiedHost;
334+
}
335+
336+
public DatabricksConfig setExperimentalIsUnifiedHost(boolean experimentalIsUnifiedHost) {
337+
this.experimentalIsUnifiedHost = experimentalIsUnifiedHost;
338+
return this;
339+
}
340+
293341
public String getDatabricksCliPath() {
294342
return this.databricksCliPath;
295343
}
@@ -670,13 +718,67 @@ public boolean isAws() {
670718
return this.getDatabricksEnvironment().getCloud() == Cloud.AWS;
671719
}
672720

721+
/**
722+
* Returns true if client is configured for Accounts API. Panics if the config has the unified
723+
* host flag set.
724+
*
725+
* @deprecated Use {@link #getHostType()} if possible, or {@link #getConfigType()} if necessary.
726+
*/
727+
@Deprecated
673728
public boolean isAccountClient() {
729+
if (getExperimentalIsUnifiedHost()) {
730+
throw new IllegalStateException(
731+
"isAccountClient cannot be used with unified hosts; use getHostType() instead");
732+
}
674733
if (host == null) {
675734
return false;
676735
}
677736
return host.startsWith("https://accounts.") || host.startsWith("https://accounts-dod.");
678737
}
679738

739+
/** Returns the type of host that the client is configured for. */
740+
public HostType getHostType() {
741+
if (getExperimentalIsUnifiedHost()) {
742+
return HostType.UNIFIED_HOST;
743+
}
744+
745+
if (host == null) {
746+
return HostType.WORKSPACE_HOST;
747+
}
748+
749+
if (host.startsWith("https://accounts.") || host.startsWith("https://accounts-dod.")) {
750+
return HostType.ACCOUNT_HOST;
751+
}
752+
753+
return HostType.WORKSPACE_HOST;
754+
}
755+
756+
/**
757+
* Returns the type of config that the client is configured for. Returns InvalidConfig if the
758+
* config is invalid. Use of this method should be avoided where possible, because we plan to
759+
* remove WorkspaceClient and AccountClient in favor of a single unified client in the future.
760+
*/
761+
public ConfigType getConfigType() {
762+
HostType hostType = getHostType();
763+
switch (hostType) {
764+
case ACCOUNT_HOST:
765+
return ConfigType.ACCOUNT_CONFIG;
766+
case WORKSPACE_HOST:
767+
return ConfigType.WORKSPACE_CONFIG;
768+
case UNIFIED_HOST:
769+
if (accountId == null || accountId.isEmpty()) {
770+
// All unified host configs must have an account ID
771+
return ConfigType.INVALID_CONFIG;
772+
}
773+
if (workspaceId != null && !workspaceId.isEmpty()) {
774+
return ConfigType.WORKSPACE_CONFIG;
775+
}
776+
return ConfigType.ACCOUNT_CONFIG;
777+
default:
778+
return ConfigType.INVALID_CONFIG;
779+
}
780+
}
781+
680782
public OpenIDConnectEndpoints getOidcEndpoints() throws IOException {
681783
if (discoveryUrl == null) {
682784
return fetchDefaultOidcEndpoints();
@@ -712,23 +814,49 @@ private OpenIDConnectEndpoints fetchDefaultOidcEndpoints() throws IOException {
712814
return new OpenIDConnectEndpoints(
713815
realAuthUrl.replaceAll("/authorize", "/token"), realAuthUrl);
714816
}
715-
if (isAccountClient() && getAccountId() != null) {
716-
String prefix = getHost() + "/oidc/accounts/" + getAccountId();
717-
return new OpenIDConnectEndpoints(prefix + "/v1/token", prefix + "/v1/authorize");
718-
}
719817

720-
ApiClient apiClient =
721-
new ApiClient.Builder()
722-
.withHttpClient(getHttpClient())
723-
.withGetHostFunc(v -> getHost())
724-
.build();
725-
try {
726-
return apiClient.execute(
727-
new Request("GET", "/oidc/.well-known/oauth-authorization-server"),
728-
OpenIDConnectEndpoints.class);
729-
} catch (IOException e) {
730-
throw new DatabricksException("IO error: " + e.getMessage(), e);
818+
HostType hostType = getHostType();
819+
switch (hostType) {
820+
case ACCOUNT_HOST:
821+
if (getAccountId() != null) {
822+
String prefix = getHost() + "/oidc/accounts/" + getAccountId();
823+
return new OpenIDConnectEndpoints(prefix + "/v1/token", prefix + "/v1/authorize");
824+
}
825+
break;
826+
case UNIFIED_HOST:
827+
if (getAccountId() != null) {
828+
ApiClient apiClient =
829+
new ApiClient.Builder()
830+
.withHttpClient(getHttpClient())
831+
.withGetHostFunc(v -> getHost())
832+
.build();
833+
try {
834+
String discoveryPath =
835+
"/oidc/accounts/" + getAccountId() + "/.well-known/oauth-authorization-server";
836+
return apiClient.execute(
837+
new Request("GET", discoveryPath), OpenIDConnectEndpoints.class);
838+
} catch (IOException e) {
839+
throw new DatabricksException("IO error: " + e.getMessage(), e);
840+
}
841+
}
842+
break;
843+
case WORKSPACE_HOST:
844+
ApiClient apiClient =
845+
new ApiClient.Builder()
846+
.withHttpClient(getHttpClient())
847+
.withGetHostFunc(v -> getHost())
848+
.build();
849+
try {
850+
return apiClient.execute(
851+
new Request("GET", "/oidc/.well-known/oauth-authorization-server"),
852+
OpenIDConnectEndpoints.class);
853+
} catch (IOException e) {
854+
throw new DatabricksException("IO error: " + e.getMessage(), e);
855+
}
856+
default:
857+
break;
731858
}
859+
return null;
732860
}
733861

734862
@Override
@@ -795,9 +923,10 @@ public DatabricksConfig newWithWorkspaceHost(String host) {
795923
Arrays.asList(
796924
// The config for WorkspaceClient has a different host and Azure Workspace resource
797925
// ID, and also omits
798-
// the account ID.
926+
// the account ID and workspace ID.
799927
"host",
800928
"accountId",
929+
"workspaceId",
801930
"azureWorkspaceResourceId",
802931
// For cloud-native OAuth, we need to reauthenticate as the audience has changed, so
803932
// don't cache the

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) {
148148
namedIdTokenSource.idTokenSource,
149149
config.getHttpClient())
150150
.audience(config.getTokenAudience())
151-
.accountId(config.isAccountClient() ? config.getAccountId() : null)
151+
.accountId(
152+
config.getHostType() != DatabricksConfig.HostType.WORKSPACE_HOST
153+
? config.getAccountId()
154+
: null)
152155
.scopes(config.getScopes())
153156
.build();
154157

databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleCredentialsCredentialsProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public HeaderFactory configure(DatabricksConfig config) {
6464
Map<String, String> headers = new HashMap<>();
6565
headers.put("Authorization", String.format("Bearer %s", idToken.getTokenValue()));
6666

67-
if (config.isAccountClient()) {
67+
if (config.getHostType() != DatabricksConfig.HostType.WORKSPACE_HOST) {
6868
AccessToken token;
6969
try {
7070
token = finalServiceAccountCredentials.createScoped(GCP_SCOPES).refreshAccessToken();

databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleIdCredentialsProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public HeaderFactory configure(DatabricksConfig config) {
6767
throw new DatabricksException(message, e);
6868
}
6969

70-
if (config.isAccountClient()) {
70+
if (config.getHostType() != DatabricksConfig.HostType.WORKSPACE_HOST) {
7171
try {
7272
headers.put(
7373
SA_ACCESS_TOKEN_HEADER, gcpScopedCredentials.refreshAccessToken().getTokenValue());

0 commit comments

Comments
 (0)