Skip to content

Commit e465191

Browse files
committed
[Feature] Support for Unified Host
1 parent 79aadac commit e465191

12 files changed

+675
-5
lines changed

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

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.databricks.sdk.core;
2+
3+
import com.databricks.sdk.support.InternalApi;
4+
5+
/**
6+
* Represents the type of Databricks client being used for API operations.
7+
*
8+
* <p>This is determined by the combination of host type and workspace ID presence:
9+
*
10+
* <ul>
11+
* <li>WORKSPACE: Can call workspace-level APIs
12+
* <li>ACCOUNT: Can call account-level APIs
13+
* <li>WORKSPACE_ON_UNIFIED: Workspace operations on a unified host (requires workspace ID)
14+
* <li>ACCOUNT_ON_UNIFIED: Account operations on a unified host
15+
* </ul>
16+
*/
17+
@InternalApi
18+
public enum ClientType {
19+
/** Traditional workspace client */
20+
WORKSPACE,
21+
22+
/** Traditional account client */
23+
ACCOUNT,
24+
25+
/** Workspace-scoped client on unified host (requires X-Databricks-Org-Id header) */
26+
WORKSPACE_ON_UNIFIED,
27+
28+
/** Account-scoped client on unified host */
29+
ACCOUNT_ON_UNIFIED
30+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
3131
}
3232
List<String> cmd =
3333
new ArrayList<>(Arrays.asList(cliPath, "auth", "token", "--host", config.getHost()));
34-
if (config.isAccountClient()) {
34+
if (config.getClientType() == ClientType.ACCOUNT
35+
|| config.getClientType() == ClientType.ACCOUNT_ON_UNIFIED) {
3536
cmd.add("--account-id");
3637
cmd.add(config.getAccountId());
3738
}

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

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@
1818
import java.util.*;
1919
import org.apache.http.HttpMessage;
2020

21+
/**
22+
* Configuration for Databricks SDK clients.
23+
*
24+
* <p>This class holds all configuration needed to authenticate and connect to Databricks services,
25+
* including support for:
26+
*
27+
* <ul>
28+
* <li>Traditional workspace and account hosts
29+
* <li>Unified hosts (SPOG) that support both workspace and account operations
30+
* <li>Multiple authentication methods (PAT, OAuth, Azure, etc.)
31+
* </ul>
32+
*
33+
* <p><b>Unified Host Support:</b> When using a unified host, set {@code experimentalIsUnifiedHost}
34+
* to {@code true} and optionally provide a {@code workspaceId} for workspace-scoped operations. Use
35+
* {@link #getHostType()} and {@link #getClientType()} instead of the deprecated {@link
36+
* #isAccountClient()} method.
37+
*/
2138
public class DatabricksConfig {
2239
private CredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
2340

@@ -27,6 +44,27 @@ public class DatabricksConfig {
2744
@ConfigAttribute(env = "DATABRICKS_ACCOUNT_ID")
2845
private String accountId;
2946

47+
/**
48+
* Workspace ID for unified host operations. When using a unified host that supports both
49+
* workspace and account-level operations, this field specifies which workspace context to operate
50+
* under for workspace-level API calls.
51+
*
52+
* <p><b>Note:</b> This API is experimental and may change or be removed in future releases
53+
* without notice.
54+
*/
55+
@ConfigAttribute(env = "DATABRICKS_WORKSPACE_ID")
56+
private String workspaceId;
57+
58+
/**
59+
* Flag to explicitly mark a host as a unified host. When true, the host is treated as supporting
60+
* both workspace and account-level operations through a single endpoint.
61+
*
62+
* <p><b>Note:</b> This API is experimental and may change or be removed in future releases
63+
* without notice.
64+
*/
65+
@ConfigAttribute(env = "DATABRICKS_EXPERIMENTAL_IS_UNIFIED_HOST")
66+
private Boolean experimentalIsUnifiedHost;
67+
3068
@ConfigAttribute(env = "DATABRICKS_TOKEN", auth = "pat", sensitive = true)
3169
private String token;
3270

@@ -233,8 +271,16 @@ public synchronized Map<String, String> authenticate() throws DatabricksExceptio
233271
if (headerFactory == null) {
234272
// Calling authenticate without resolve
235273
ConfigLoader.fixHostIfNeeded(this);
236-
headerFactory = credentialsProvider.configure(this);
274+
HeaderFactory rawHeaderFactory = credentialsProvider.configure(this);
237275
setAuthType(credentialsProvider.authType());
276+
277+
// For unified hosts with workspace operations, wrap the header factory
278+
// to inject the X-Databricks-Org-Id header
279+
if (getClientType() == ClientType.WORKSPACE_ON_UNIFIED) {
280+
headerFactory = new UnifiedHostHeaderFactory(rawHeaderFactory, workspaceId);
281+
} else {
282+
headerFactory = rawHeaderFactory;
283+
}
238284
}
239285
return headerFactory.headers();
240286
} catch (DatabricksException e) {
@@ -298,6 +344,24 @@ public DatabricksConfig setAccountId(String accountId) {
298344
return this;
299345
}
300346

347+
public String getWorkspaceId() {
348+
return workspaceId;
349+
}
350+
351+
public DatabricksConfig setWorkspaceId(String workspaceId) {
352+
this.workspaceId = workspaceId;
353+
return this;
354+
}
355+
356+
public Boolean getExperimentalIsUnifiedHost() {
357+
return experimentalIsUnifiedHost;
358+
}
359+
360+
public DatabricksConfig setExperimentalIsUnifiedHost(Boolean experimentalIsUnifiedHost) {
361+
this.experimentalIsUnifiedHost = experimentalIsUnifiedHost;
362+
return this;
363+
}
364+
301365
public String getDatabricksCliPath() {
302366
return this.databricksCliPath;
303367
}
@@ -679,12 +743,73 @@ public boolean isAws() {
679743
}
680744

681745
public boolean isAccountClient() {
746+
if (getHostType() == HostType.UNIFIED) {
747+
throw new DatabricksException(
748+
"Cannot determine account client status for unified hosts. "
749+
+ "Use getHostType() or getClientType() instead. "
750+
+ "For unified hosts, client type depends on whether workspaceId is set.");
751+
}
682752
if (host == null) {
683753
return false;
684754
}
685755
return host.startsWith("https://accounts.") || host.startsWith("https://accounts-dod.");
686756
}
687757

758+
/**
759+
* Determines the type of host based on configuration settings and host URL.
760+
*
761+
* <p>Detection logic:
762+
*
763+
* <ol>
764+
* <li>If experimentalIsUnifiedHost is true → UNIFIED
765+
* <li>If host starts with "accounts." or "accounts-dod." → ACCOUNTS
766+
* <li>Otherwise → WORKSPACE
767+
* </ol>
768+
*
769+
* @return The detected host type
770+
*/
771+
public HostType getHostType() {
772+
if (experimentalIsUnifiedHost != null && experimentalIsUnifiedHost) {
773+
return HostType.UNIFIED;
774+
}
775+
if (host == null) {
776+
return HostType.WORKSPACE;
777+
}
778+
if (host.startsWith("https://accounts.") || host.startsWith("https://accounts-dod.")) {
779+
return HostType.ACCOUNTS;
780+
}
781+
return HostType.WORKSPACE;
782+
}
783+
784+
/**
785+
* Determines the client type based on host type and workspace ID configuration.
786+
*
787+
* <p>Client type logic:
788+
*
789+
* <ul>
790+
* <li>UNIFIED host + workspaceId set → WORKSPACE_ON_UNIFIED
791+
* <li>UNIFIED host + no workspaceId → ACCOUNT_ON_UNIFIED
792+
* <li>ACCOUNTS host → ACCOUNT
793+
* <li>WORKSPACE host → WORKSPACE
794+
* </ul>
795+
*
796+
* @return The determined client type
797+
*/
798+
public ClientType getClientType() {
799+
HostType hostType = getHostType();
800+
switch (hostType) {
801+
case UNIFIED:
802+
return (workspaceId != null && !workspaceId.isEmpty())
803+
? ClientType.WORKSPACE_ON_UNIFIED
804+
: ClientType.ACCOUNT_ON_UNIFIED;
805+
case ACCOUNTS:
806+
return ClientType.ACCOUNT;
807+
case WORKSPACE:
808+
default:
809+
return ClientType.WORKSPACE;
810+
}
811+
}
812+
688813
public OpenIDConnectEndpoints getOidcEndpoints() throws IOException {
689814
if (discoveryUrl == null) {
690815
return fetchDefaultOidcEndpoints();
@@ -705,10 +830,36 @@ private OpenIDConnectEndpoints fetchOidcEndpointsFromDiscovery() {
705830
return null;
706831
}
707832

833+
/**
834+
* Fetches OIDC endpoints for unified hosts using the account ID.
835+
*
836+
* <p>For unified hosts, the OIDC endpoints follow the pattern:
837+
* {host}/oidc/accounts/{accountId}/v1/{token|authorize}
838+
*
839+
* @param accountId The account ID to use for endpoint construction
840+
* @return OpenIDConnectEndpoints configured for the unified host
841+
* @throws DatabricksException if accountId is null or empty
842+
* @throws IOException if endpoint construction fails
843+
*/
844+
private OpenIDConnectEndpoints getUnifiedOidcEndpoints(String accountId) throws IOException {
845+
if (accountId == null || accountId.isEmpty()) {
846+
throw new DatabricksException(
847+
"account_id is required for unified host OIDC endpoint discovery");
848+
}
849+
String prefix = getHost() + "/oidc/accounts/" + accountId;
850+
return new OpenIDConnectEndpoints(prefix + "/v1/token", prefix + "/v1/authorize");
851+
}
852+
708853
private OpenIDConnectEndpoints fetchDefaultOidcEndpoints() throws IOException {
709854
if (getHost() == null) {
710855
return null;
711856
}
857+
858+
// For unified hosts, use account-based OIDC endpoints
859+
if (getHostType() == HostType.UNIFIED) {
860+
return getUnifiedOidcEndpoints(getAccountId());
861+
}
862+
712863
if (isAzure() && getAzureClientId() != null) {
713864
Request request = new Request("GET", getHost() + "/oidc/oauth2/v2.0/authorize");
714865
request.setRedirectionBehavior(false);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) {
150150
namedIdTokenSource.idTokenSource,
151151
config.getHttpClient())
152152
.audience(config.getTokenAudience())
153-
.accountId(config.isAccountClient() ? config.getAccountId() : null)
153+
.accountId(
154+
(config.getClientType() == ClientType.ACCOUNT
155+
|| config.getClientType() == ClientType.ACCOUNT_ON_UNIFIED)
156+
? config.getAccountId()
157+
: null)
154158
.scopes(config.getScopes())
155159
.build();
156160

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

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

69-
if (config.isAccountClient()) {
69+
if (config.getClientType() == ClientType.ACCOUNT
70+
|| config.getClientType() == ClientType.ACCOUNT_ON_UNIFIED) {
7071
AccessToken token;
7172
try {
7273
token = finalServiceAccountCredentials.createScoped(GCP_SCOPES).refreshAccessToken();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public HeaderFactory configure(DatabricksConfig config) {
6969
throw new DatabricksException(message, e);
7070
}
7171

72-
if (config.isAccountClient()) {
72+
if (config.getClientType() == ClientType.ACCOUNT
73+
|| config.getClientType() == ClientType.ACCOUNT_ON_UNIFIED) {
7374
try {
7475
headers.put(
7576
SA_ACCESS_TOKEN_HEADER, gcpScopedCredentials.refreshAccessToken().getTokenValue());
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.databricks.sdk.core;
2+
3+
import com.databricks.sdk.support.InternalApi;
4+
5+
/**
6+
* Represents the type of Databricks host being used.
7+
*
8+
* <p>This determines which APIs are available and how authentication should be handled:
9+
*
10+
* <ul>
11+
* <li>WORKSPACE: Traditional workspace host (e.g., adb-*.azuredatabricks.net)
12+
* <li>ACCOUNTS: Traditional account host (e.g., accounts.cloud.databricks.com)
13+
* <li>UNIFIED: Unified host supporting both workspace and account operations
14+
* </ul>
15+
*/
16+
@InternalApi
17+
public enum HostType {
18+
/** Traditional workspace host - supports workspace-level APIs only */
19+
WORKSPACE,
20+
21+
/** Traditional accounts host - supports account-level APIs only */
22+
ACCOUNTS,
23+
24+
/** Unified host - supports both workspace and account APIs based on context */
25+
UNIFIED
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.databricks.sdk.core;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* HeaderFactory wrapper that adds X-Databricks-Org-Id header for unified host workspace operations.
8+
*
9+
* <p>When making workspace-level API calls to a unified host, this header is required to specify
10+
* which workspace context the operation should execute in.
11+
*/
12+
class UnifiedHostHeaderFactory implements HeaderFactory {
13+
private final HeaderFactory delegate;
14+
private final String workspaceId;
15+
16+
/**
17+
* Creates a new unified host header factory.
18+
*
19+
* @param delegate The underlying header factory (e.g., OAuth, PAT)
20+
* @param workspaceId The workspace ID to inject in the X-Databricks-Org-Id header
21+
*/
22+
public UnifiedHostHeaderFactory(HeaderFactory delegate, String workspaceId) {
23+
if (delegate == null) {
24+
throw new IllegalArgumentException("delegate cannot be null");
25+
}
26+
if (workspaceId == null || workspaceId.isEmpty()) {
27+
throw new IllegalArgumentException("workspaceId cannot be null or empty");
28+
}
29+
this.delegate = delegate;
30+
this.workspaceId = workspaceId;
31+
}
32+
33+
@Override
34+
public Map<String, String> headers() {
35+
Map<String, String> headers = new HashMap<>(delegate.headers());
36+
headers.put("X-Databricks-Org-Id", workspaceId);
37+
return headers;
38+
}
39+
}

0 commit comments

Comments
 (0)