Skip to content

Commit 3c4dd89

Browse files
Add support for direct dataplane access (#453)
## What changes are proposed in this pull request? This PR introduces direct dataplane access to the Java SDK. ## Key Changes ### 1. OAuth Header Factory Implementation - Introduced `OAuthHeaderFactory` interface that combines `HeaderFactory` and `TokenSource` functionality ### 2. Error Token Source - Added `ErrorTokenSource` implementation that provides clear error messages when OAuth tokens are not supported ### 3. OAuth Authentication Flow Changes - Modified OAuth auth types to return `OAuthHeaderFactory` instead of basic `HeaderFactory` - Updated credential providers to use the new OAuth header factory system: - Azure Service Principal - OAuth M2M Service Principal - External Browser - Databricks CLI - GitHub OIDC - Azure GitHub OIDC ## How is this tested? Added unit tests for all new classes including `OAuthHeaderFactory`, `ErrorTokenSource`, along with tests for `config.getTokenSource()`. The feature has been manually validated end-to-end by creating a model serving endpoint with route optimization enabled and successfully sending queries through the dataplane access path. --------- Co-authored-by: Renaud Hartert <[email protected]>
1 parent c7721fa commit 3c4dd89

25 files changed

+785
-385
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Release v0.52.0
44

55
### New Features and Improvements
6+
* Added Direct-to-Dataplane API support, allowing users to query route optimized model serving endpoints ([#453](https://github.com/databricks/databricks-sdk-java/pull/453)).
67

78
### Bug Fixes
89

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

Lines changed: 27 additions & 1 deletion
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: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.databricks.sdk.core.error.PrivateLinkInfo;
55
import com.databricks.sdk.core.http.HttpClient;
66
import com.databricks.sdk.core.http.Request;
7+
import com.databricks.sdk.core.http.RequestOptions;
78
import com.databricks.sdk.core.http.Response;
89
import com.databricks.sdk.core.retry.RequestBasedRetryStrategyPicker;
910
import com.databricks.sdk.core.retry.RetryStrategy;
@@ -50,7 +51,6 @@ public Builder withDatabricksConfig(DatabricksConfig config) {
5051
this.accountId = config.getAccountId();
5152
this.retryStrategyPicker = new RequestBasedRetryStrategyPicker(config.getHost());
5253
this.isDebugHeaders = config.isDebugHeaders();
53-
5454
return this;
5555
}
5656

@@ -173,33 +173,42 @@ public Map<String, String> getStringMap(Request req) {
173173

174174
protected <I, O> O withJavaType(Request request, JavaType javaType) {
175175
try {
176-
Response response = getResponse(request);
176+
Response response = executeInner(request, request.getUrl(), new RequestOptions());
177177
return deserialize(response.getBody(), javaType);
178178
} catch (IOException e) {
179179
throw new DatabricksException("IO error: " + e.getMessage(), e);
180180
}
181181
}
182182

183183
/**
184-
* Executes HTTP request with retries and converts it to proper POJO
184+
* Executes HTTP request with retries and converts it to proper POJO.
185185
*
186186
* @param in Commons HTTP request
187187
* @param target Expected pojo type
188188
* @return POJO of requested type
189189
*/
190190
public <T> T execute(Request in, Class<T> target) throws IOException {
191-
Response out = getResponse(in);
191+
return execute(in, target, new RequestOptions());
192+
}
193+
194+
/**
195+
* Executes HTTP request with retries and converts it to proper POJO, using custom request
196+
* options.
197+
*
198+
* @param in Commons HTTP request
199+
* @param target Expected pojo type
200+
* @param options Optional request options to customize request behavior
201+
* @return POJO of requested type
202+
*/
203+
public <T> T execute(Request in, Class<T> target, RequestOptions options) throws IOException {
204+
Response out = executeInner(in, in.getUrl(), options);
192205
if (target == Void.class) {
193206
return null;
194207
}
195208
return deserialize(out, target);
196209
}
197210

198-
private Response getResponse(Request in) {
199-
return executeInner(in, in.getUrl());
200-
}
201-
202-
private Response executeInner(Request in, String path) {
211+
private Response executeInner(Request in, String path, RequestOptions options) {
203212
RetryStrategy retryStrategy = retryStrategyPicker.getRetryStrategy(in);
204213
int attemptNumber = 0;
205214
while (true) {
@@ -224,6 +233,8 @@ private Response executeInner(Request in, String path) {
224233
}
225234
in.withHeader("User-Agent", userAgent);
226235

236+
options.applyOptions(in);
237+
227238
// Make the request, catching any exceptions, as we may want to retry.
228239
try {
229240
out = httpClient.execute(in);
@@ -434,4 +445,8 @@ public String serialize(Object body) throws JsonProcessingException {
434445
}
435446
return mapper.writeValueAsString(body);
436447
}
448+
449+
public HttpClient getHttpClient() {
450+
return httpClient;
451+
}
437452
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.databricks.sdk.core;
22

3+
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
34
import com.databricks.sdk.core.oauth.Token;
45
import com.databricks.sdk.core.utils.AzureUtils;
56
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -68,7 +69,7 @@ private Optional<String> getSubscription(DatabricksConfig config) {
6869
}
6970

7071
@Override
71-
public HeaderFactory configure(DatabricksConfig config) {
72+
public OAuthHeaderFactory configure(DatabricksConfig config) {
7273
if (!config.isAzure()) {
7374
return null;
7475
}
@@ -86,15 +87,17 @@ public HeaderFactory configure(DatabricksConfig config) {
8687
mgmtTokenSource = null;
8788
}
8889
CliTokenSource finalMgmtTokenSource = mgmtTokenSource;
89-
return () -> {
90-
Token token = tokenSource.getToken();
91-
Map<String, String> headers = new HashMap<>();
92-
headers.put("Authorization", token.getTokenType() + " " + token.getAccessToken());
93-
if (finalMgmtTokenSource != null) {
94-
AzureUtils.addSpManagementToken(finalMgmtTokenSource, headers);
95-
}
96-
return AzureUtils.addWorkspaceResourceId(config, headers);
97-
};
90+
return OAuthHeaderFactory.fromSuppliers(
91+
tokenSource::getToken,
92+
() -> {
93+
Token token = tokenSource.getToken();
94+
Map<String, String> headers = new HashMap<>();
95+
headers.put("Authorization", token.getTokenType() + " " + token.getAccessToken());
96+
if (finalMgmtTokenSource != null) {
97+
AzureUtils.addSpManagementToken(finalMgmtTokenSource, headers);
98+
}
99+
return AzureUtils.addWorkspaceResourceId(config, headers);
100+
});
98101
} catch (DatabricksException e) {
99102
String stderr = e.getMessage();
100103
if (stderr.contains("not found")) {

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.databricks.sdk.core;
22

3-
import com.databricks.sdk.core.oauth.Token;
3+
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
44
import com.databricks.sdk.core.utils.OSUtils;
55
import java.util.*;
66
import org.slf4j.Logger;
@@ -36,7 +36,7 @@ private CliTokenSource getDatabricksCliTokenSource(DatabricksConfig config) {
3636
}
3737

3838
@Override
39-
public HeaderFactory configure(DatabricksConfig config) {
39+
public OAuthHeaderFactory configure(DatabricksConfig config) {
4040
String host = config.getHost();
4141
if (host == null) {
4242
return null;
@@ -48,12 +48,7 @@ public HeaderFactory configure(DatabricksConfig config) {
4848
return null;
4949
}
5050
tokenSource.getToken(); // We need this for checking if databricks CLI is installed.
51-
return () -> {
52-
Token token = tokenSource.getToken();
53-
Map<String, String> headers = new HashMap<>();
54-
headers.put("Authorization", token.getTokenType() + " " + token.getAccessToken());
55-
return headers;
56-
};
51+
return OAuthHeaderFactory.fromTokenSource(tokenSource);
5752
} catch (DatabricksException e) {
5853
String stderr = e.getMessage();
5954
if (stderr.contains("not found")) {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import com.databricks.sdk.core.http.HttpClient;
55
import com.databricks.sdk.core.http.Request;
66
import com.databricks.sdk.core.http.Response;
7+
import com.databricks.sdk.core.oauth.ErrorTokenSource;
8+
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
79
import com.databricks.sdk.core.oauth.OpenIDConnectEndpoints;
10+
import com.databricks.sdk.core.oauth.TokenSource;
811
import com.databricks.sdk.core.utils.Cloud;
912
import com.databricks.sdk.core.utils.Environment;
1013
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -209,6 +212,24 @@ public synchronized Map<String, String> authenticate() throws DatabricksExceptio
209212
}
210213
}
211214

215+
public TokenSource getTokenSource() {
216+
if (headerFactory == null) {
217+
try {
218+
ConfigLoader.fixHostIfNeeded(this);
219+
headerFactory = credentialsProvider.configure(this);
220+
} catch (Exception e) {
221+
return new ErrorTokenSource("Failed to get token source: " + e.getMessage());
222+
}
223+
setAuthType(credentialsProvider.authType());
224+
}
225+
226+
if (headerFactory instanceof OAuthHeaderFactory) {
227+
return (TokenSource) headerFactory;
228+
}
229+
return new ErrorTokenSource(
230+
String.format("OAuth Token not supported for current auth type %s", authType));
231+
}
232+
212233
public CredentialsProvider getCredentialsProvider() {
213234
return this.credentialsProvider;
214235
}

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,6 @@ public NamedIDTokenSource(String name, IDTokenSource idTokenSource) {
3434
this.name = name;
3535
this.idTokenSource = idTokenSource;
3636
}
37-
38-
public String getName() {
39-
return name;
40-
}
41-
42-
public IDTokenSource getIdTokenSource() {
43-
return idTokenSource;
44-
}
4537
}
4638

4739
public DefaultCredentialsProvider() {}
@@ -143,14 +135,13 @@ private void addOIDCCredentialsProviders(DatabricksConfig config) {
143135
config.getClientId(),
144136
config.getHost(),
145137
endpoints,
146-
namedIdTokenSource.getIdTokenSource(),
138+
namedIdTokenSource.idTokenSource,
147139
config.getHttpClient())
148140
.audience(config.getTokenAudience())
149141
.accountId(config.isAccountClient() ? config.getAccountId() : null)
150142
.build();
151143

152-
providers.add(
153-
new TokenSourceCredentialsProvider(oauthTokenSource, namedIdTokenSource.getName()));
144+
providers.add(new TokenSourceCredentialsProvider(oauthTokenSource, namedIdTokenSource.name));
154145
}
155146
}
156147

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.databricks.sdk.core.http;
2+
3+
import java.util.function.Function;
4+
5+
/**
6+
* A builder class for configuring HTTP request transformations including authentication, URL, and
7+
* user agent headers.
8+
*
9+
* <p>Experimental: this class is experimental and subject to change in backward incompatible ways.
10+
*/
11+
public class RequestOptions {
12+
private Function<Request, Request> authenticateFunc;
13+
private Function<Request, Request> urlFunc;
14+
private Function<Request, Request> userAgentFunc;
15+
16+
/**
17+
* Constructs a new RequestOptions instance with default identity functions. Initially, all
18+
* transformations are set to pass through the request unchanged.
19+
*/
20+
public RequestOptions() {
21+
// Default to identity functions
22+
this.authenticateFunc = request -> request;
23+
this.urlFunc = request -> request;
24+
this.userAgentFunc = request -> request;
25+
}
26+
27+
/**
28+
* Sets the authorization header for the request.
29+
*
30+
* @param authorization The authorization value to be set in the header
31+
* @return This RequestOptions instance for method chaining
32+
*/
33+
public RequestOptions withAuthorization(String authorization) {
34+
this.authenticateFunc = request -> request.withHeader("Authorization", authorization);
35+
return this;
36+
}
37+
38+
/**
39+
* Sets the URL for the request.
40+
*
41+
* @param url The URL to be set for the request
42+
* @return This RequestOptions instance for method chaining
43+
*/
44+
public RequestOptions withUrl(String url) {
45+
this.urlFunc = request -> request.withUrl(url);
46+
return this;
47+
}
48+
49+
/**
50+
* Sets the User-Agent header for the request.
51+
*
52+
* @param userAgent The user agent string to be set in the header
53+
* @return This RequestOptions instance for method chaining
54+
*/
55+
public RequestOptions withUserAgent(String userAgent) {
56+
this.userAgentFunc = request -> request.withHeader("User-Agent", userAgent);
57+
return this;
58+
}
59+
60+
/**
61+
* Applies all configured transformations to the given request. The transformations are applied in
62+
* the following order: 1. Authentication 2. URL 3. User-Agent
63+
*
64+
* @param request The original request to be transformed
65+
* @return A new Request instance with all transformations applied
66+
*/
67+
public Request applyOptions(Request request) {
68+
// Apply all transformation functions in sequence
69+
return userAgentFunc.apply(urlFunc.apply(authenticateFunc.apply(request)));
70+
}
71+
}

0 commit comments

Comments
 (0)