Skip to content

Commit 47ea8a7

Browse files
Call resolveHostMetadata on Config init (#713)
## 🥞 Stacked PR Use this [link](https://github.com/databricks/databricks-sdk-java/pull/713/files/12f05320deaf1e2d96229e7bb280ecf7c59b25ce..f5a4892cb3877c74bd8cff5979a3a2d177d304ff) to review incremental changes. - [hectorcast-db/stack/port-3-test-get-workspace-client-spog](#712) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/712/files)] - [**hectorcast-db/stack/port-4-resolve-metadata-on-init**](#713) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/713/files/12f05320deaf1e2d96229e7bb280ecf7c59b25ce..f5a4892cb3877c74bd8cff5979a3a2d177d304ff)] - [hectorcast-db/stack/port-5-token-audience-from-metadata](#714) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/714/files/f5a4892cb3877c74bd8cff5979a3a2d177d304ff..513d3f937652fe2a92564fddbb50a46b0527cf97)] - [hectorcast-db/stack/port-6-gcp-sa-nonblocking](#718) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/718/files/513d3f937652fe2a92564fddbb50a46b0527cf97..560f2173f1ac8880634d9ad874a72824903a91e9)] - [hectorcast-db/stack/port-7-integration-test-metadata](#719) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/719/files/560f2173f1ac8880634d9ad874a72824903a91e9..f79a3e876905d11de94c5f8c589b2af702397cd3)] - [hectorcast-db/stack/port-8-remove-unified-flag](#720) [[Files changed](https://github.com/databricks/databricks-sdk-java/pull/720/files/f79a3e876905d11de94c5f8c589b2af702397cd3..3c63fc8995cba2381947a52f485bef2cb17693a3)] --------- ## Summary Port of Go SDK [#1542](databricks/databricks-sdk-go#1542). Calls `resolveHostMetadata()` during config `resolve()` to populate `accountId`, `workspaceId`, and `discoveryUrl` from the host's `/.well-known/databricks-config` endpoint. Failures are logged at debug level and do not block initialization. **Why:** Previously, host metadata was only resolved on explicit call. Now it's resolved automatically during config init, so OIDC endpoints, account IDs, and workspace IDs are populated from the authoritative discovery endpoint. **Changes:** - `DatabricksConfig.innerResolve()`: calls `tryResolveHostMetadata()` after HTTP client init - `tryResolveHostMetadata()`: catches `Throwable` (not just Exception) to handle mock assertion errors in tests - `clone()`: skips static fields (needed for new Logger) - Tests: FixtureServer-based tests updated to add `/.well-known/databricks-config` fixture `NO_CHANGELOG=true` ## Test plan - [x] `DatabricksConfigTest`: 45+ tests pass - [x] All 1086 tests pass
1 parent 8182b4b commit 47ea8a7

File tree

2 files changed

+149
-24
lines changed

2 files changed

+149
-24
lines changed

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

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515
import java.io.File;
1616
import java.io.IOException;
1717
import java.lang.reflect.Field;
18+
import java.net.URI;
1819
import java.time.Duration;
1920
import java.util.*;
2021
import org.apache.http.HttpMessage;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
2124

2225
public class DatabricksConfig {
26+
private static final Logger LOG = LoggerFactory.getLogger(DatabricksConfig.class);
27+
2328
private CredentialsProvider credentialsProvider = new DefaultCredentialsProvider();
2429

2530
@ConfigAttribute(env = "DATABRICKS_HOST")
@@ -219,12 +224,32 @@ private synchronized DatabricksConfig innerResolve() {
219224
sortScopes();
220225
ConfigLoader.fixHostIfNeeded(this);
221226
initHttp();
227+
tryResolveHostMetadata();
222228
return this;
223229
} catch (DatabricksException e) {
224230
throw ConfigLoader.makeNicerError(e.getMessage(), e, this);
225231
}
226232
}
227233

234+
/**
235+
* Attempts to resolve host metadata from the well-known endpoint. Only called for unified hosts.
236+
* Logs a warning and continues if metadata resolution fails, since not all hosts support the
237+
* discovery endpoint.
238+
*/
239+
private void tryResolveHostMetadata() {
240+
if (host == null) {
241+
return;
242+
}
243+
if (experimentalIsUnifiedHost == null || !experimentalIsUnifiedHost) {
244+
return;
245+
}
246+
try {
247+
resolveHostMetadata();
248+
} catch (Exception e) {
249+
LOG.warn("Failed to resolve host metadata: {}. Falling back to user config.", e.getMessage());
250+
}
251+
}
252+
228253
// Sort scopes in-place for better de-duplication in the refresh token cache.
229254
private void sortScopes() {
230255
if (scopes != null && !scopes.isEmpty()) {
@@ -832,37 +857,46 @@ HostMetadata getHostMetadata() throws IOException {
832857
* discovery endpoint.
833858
*
834859
* <p>Fills in {@code accountId}, {@code workspaceId}, and {@code discoveryUrl} (derived from
835-
* {@code oidc_endpoint}, with any {@code {account_id}} placeholder substituted) if not already
836-
* set.
860+
* {@code oidc_endpoint}, with {@code /.well-known/oauth-authorization-server} appended and any
861+
* {@code {account_id}} placeholder substituted) if not already set.
862+
*
863+
* <p>Errors from the metadata endpoint are non-fatal: a warning is logged and the method returns
864+
* without modifying the config. This mirrors the Go SDK behavior where metadata resolution is
865+
* best-effort during config init.
837866
*
838867
* <p><b>Note:</b> This API is experimental and may change or be removed in future releases
839868
* without notice.
840-
*
841-
* @throws DatabricksException if {@code accountId} cannot be resolved or {@code oidc_endpoint} is
842-
* missing from the host metadata.
843869
*/
844870
void resolveHostMetadata() throws IOException {
845871
if (host == null) {
846872
return;
847873
}
848874
HostMetadata meta = getHostMetadata();
849875
if (accountId == null && meta.getAccountId() != null) {
876+
LOG.debug("Resolved account_id from host metadata: \"{}\"", meta.getAccountId());
850877
accountId = meta.getAccountId();
851878
}
852-
if (accountId == null) {
853-
throw new DatabricksException(
854-
"account_id is not configured and could not be resolved from host metadata");
855-
}
856879
if (workspaceId == null && meta.getWorkspaceId() != null) {
880+
LOG.debug("Resolved workspace_id from host metadata: \"{}\"", meta.getWorkspaceId());
857881
workspaceId = meta.getWorkspaceId();
858882
}
859883
if (discoveryUrl == null) {
860-
if (meta.getOidcEndpoint() != null && !meta.getOidcEndpoint().isEmpty()) {
861-
discoveryUrl = meta.getOidcEndpoint().replace("{account_id}", accountId);
862-
} else {
863-
throw new DatabricksException(
864-
"discovery_url is not configured and could not be resolved from host metadata");
884+
if (meta.getOidcEndpoint() == null || meta.getOidcEndpoint().isEmpty()) {
885+
LOG.warn("Host metadata missing oidc_endpoint; skipping discovery URL resolution");
886+
return;
887+
}
888+
String oidcRoot = meta.getOidcEndpoint();
889+
if (oidcRoot.contains("{account_id}")) {
890+
if (accountId == null || accountId.isEmpty()) {
891+
LOG.warn(
892+
"Host metadata oidc_endpoint contains {account_id} placeholder but account_id is not set; skipping discovery URL resolution");
893+
return;
894+
}
895+
oidcRoot = oidcRoot.replace("{account_id}", accountId);
865896
}
897+
URI oidcUri = URI.create(oidcRoot.endsWith("/") ? oidcRoot : oidcRoot + "/");
898+
discoveryUrl = oidcUri.resolve(".well-known/oauth-authorization-server").toString();
899+
LOG.debug("Resolved discovery_url from host metadata: \"{}\"", discoveryUrl);
866900
}
867901
}
868902

@@ -962,6 +996,10 @@ private DatabricksConfig clone(Set<String> fieldsToSkip) {
962996
if (fieldsToSkip.contains(f.getName())) {
963997
continue;
964998
}
999+
// Skip static fields (e.g. LOG) — they are shared across instances and cannot be set.
1000+
if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) {
1001+
continue;
1002+
}
9651003
try {
9661004
f.set(newConfig, f.get(this));
9671005
} catch (IllegalAccessException e) {

databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,9 @@ public void testResolveHostMetadataWorkspacePopulatesAllFields() throws IOExcept
499499
config.resolveHostMetadata();
500500
assertEquals(DUMMY_ACCOUNT_ID, config.getAccountId());
501501
assertEquals(DUMMY_WORKSPACE_ID, config.getWorkspaceId());
502-
assertEquals("https://ws.databricks.com/oidc", config.getDiscoveryUrl());
502+
assertEquals(
503+
"https://ws.databricks.com/oidc/.well-known/oauth-authorization-server",
504+
config.getDiscoveryUrl());
503505
}
504506
}
505507

@@ -514,7 +516,10 @@ public void testResolveHostMetadataAccountSubstitutesAccountId() throws IOExcept
514516
config.resolve(emptyEnv());
515517
config.resolveHostMetadata();
516518
assertEquals(
517-
"https://acc.databricks.com/oidc/accounts/" + DUMMY_ACCOUNT_ID, config.getDiscoveryUrl());
519+
"https://acc.databricks.com/oidc/accounts/"
520+
+ DUMMY_ACCOUNT_ID
521+
+ "/.well-known/oauth-authorization-server",
522+
config.getDiscoveryUrl());
518523
}
519524
}
520525

@@ -541,29 +546,31 @@ public void testResolveHostMetadataDoesNotOverwriteExistingFields() throws IOExc
541546
}
542547

543548
@Test
544-
public void testResolveHostMetadataRaisesWhenAccountIdUnresolvable() throws IOException {
549+
public void testResolveHostMetadataMissingAccountIdWithPlaceholderSkipsDiscoveryUrl()
550+
throws IOException {
545551
String response =
546552
"{\"oidc_endpoint\":\"https://acc.databricks.com/oidc/accounts/{account_id}\"}";
547553
try (FixtureServer server =
548554
new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) {
549555
DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl());
550556
config.resolve(emptyEnv());
551-
DatabricksException ex =
552-
assertThrows(DatabricksException.class, () -> config.resolveHostMetadata());
553-
assertTrue(ex.getMessage().contains("account_id is not configured"));
557+
config.resolveHostMetadata();
558+
// DiscoveryURL should not be set because account_id is empty and placeholder can't be
559+
// substituted
560+
assertNull(config.getDiscoveryUrl());
554561
}
555562
}
556563

557564
@Test
558-
public void testResolveHostMetadataRaisesWhenOidcEndpointMissing() throws IOException {
565+
public void testResolveHostMetadataNoOidcEndpointSkipsDiscoveryUrl() throws IOException {
559566
String response = "{\"account_id\":\"" + DUMMY_ACCOUNT_ID + "\"}";
560567
try (FixtureServer server =
561568
new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) {
562569
DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl());
563570
config.resolve(emptyEnv());
564-
DatabricksException ex =
565-
assertThrows(DatabricksException.class, () -> config.resolveHostMetadata());
566-
assertTrue(ex.getMessage().contains("discovery_url is not configured"));
571+
config.resolveHostMetadata();
572+
assertEquals(DUMMY_ACCOUNT_ID, config.getAccountId());
573+
assertNull(config.getDiscoveryUrl());
567574
}
568575
}
569576

@@ -579,6 +586,86 @@ public void testResolveHostMetadataRaisesOnHttpError() throws IOException {
579586
}
580587
}
581588

589+
// --- tryResolveHostMetadata (config init) tests ---
590+
591+
@Test
592+
public void testEnsureResolvedResolvesHostMetadataWhenUnifiedHost() throws IOException {
593+
String response =
594+
"{\"oidc_endpoint\":\"https://ws.databricks.com/oidc\","
595+
+ "\"account_id\":\""
596+
+ DUMMY_ACCOUNT_ID
597+
+ "\","
598+
+ "\"workspace_id\":\""
599+
+ DUMMY_WORKSPACE_ID
600+
+ "\"}";
601+
try (FixtureServer server =
602+
new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) {
603+
DatabricksConfig config =
604+
new DatabricksConfig().setHost(server.getUrl()).setExperimentalIsUnifiedHost(true);
605+
config.resolve(emptyEnv());
606+
assertEquals(DUMMY_ACCOUNT_ID, config.getAccountId());
607+
assertEquals(DUMMY_WORKSPACE_ID, config.getWorkspaceId());
608+
assertEquals(
609+
"https://ws.databricks.com/oidc/.well-known/oauth-authorization-server",
610+
config.getDiscoveryUrl());
611+
}
612+
}
613+
614+
@Test
615+
public void testEnsureResolvedSkipsHostMetadataWhenNotUnified() throws IOException {
616+
// No metadata endpoint fixture — if it were called, the fixture server would fail
617+
try (FixtureServer server = new FixtureServer()) {
618+
DatabricksConfig config = new DatabricksConfig().setHost(server.getUrl());
619+
config.resolve(emptyEnv());
620+
assertNull(config.getAccountId());
621+
assertNull(config.getWorkspaceId());
622+
}
623+
}
624+
625+
@Test
626+
public void testEnsureResolvedHostMetadataFailureNonFatal() throws IOException {
627+
try (FixtureServer server =
628+
new FixtureServer()
629+
.with(
630+
"GET", "/.well-known/databricks-config", "{\"error\": \"internal error\"}", 500)) {
631+
DatabricksConfig config =
632+
new DatabricksConfig().setHost(server.getUrl()).setExperimentalIsUnifiedHost(true);
633+
// Should not throw — metadata failure is non-fatal
634+
config.resolve(emptyEnv());
635+
assertNull(config.getAccountId());
636+
assertNull(config.getWorkspaceId());
637+
}
638+
}
639+
640+
@Test
641+
public void testEnsureResolvedHostMetadataNoOidcEndpointNonFatal() throws IOException {
642+
String response = "{\"account_id\":\"" + DUMMY_ACCOUNT_ID + "\"}";
643+
try (FixtureServer server =
644+
new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) {
645+
DatabricksConfig config =
646+
new DatabricksConfig().setHost(server.getUrl()).setExperimentalIsUnifiedHost(true);
647+
config.resolve(emptyEnv());
648+
assertEquals(DUMMY_ACCOUNT_ID, config.getAccountId());
649+
assertNull(config.getDiscoveryUrl());
650+
}
651+
}
652+
653+
@Test
654+
public void testEnsureResolvedHostMetadataMissingAccountIdWithPlaceholderNonFatal()
655+
throws IOException {
656+
String response =
657+
"{\"oidc_endpoint\":\"https://acc.databricks.com/oidc/accounts/{account_id}\"}";
658+
try (FixtureServer server =
659+
new FixtureServer().with("GET", "/.well-known/databricks-config", response, 200)) {
660+
DatabricksConfig config =
661+
new DatabricksConfig().setHost(server.getUrl()).setExperimentalIsUnifiedHost(true);
662+
config.resolve(emptyEnv());
663+
// DiscoveryURL should not be set because account_id is empty and placeholder can't be
664+
// substituted
665+
assertNull(config.getDiscoveryUrl());
666+
}
667+
}
668+
582669
// --- discoveryUrl / OIDC endpoint tests ---
583670

584671
@Test

0 commit comments

Comments
 (0)