Skip to content

Commit bfb25d4

Browse files
simonfaltumclaude
andauthored
Add support for default_profile in [__settings__] section (#698)
## Summary - When no profile is explicitly configured, check `[__settings__].default_profile` in `~/.databrickscfg` before falling back to the `DEFAULT` section - Aligns the Java SDK with the CLI's `auth switch` feature Profile resolution order: 1. Explicit profile (`--profile` flag, env var, or programmatic config) 2. `[__settings__].default_profile` (new) 3. `[DEFAULT]` section (legacy fallback) ## Changes **`ConfigLoader.java`**: Extracted a `resolveProfile` method that encapsulates all profile resolution logic (explicit, `[__settings__].default_profile`, DEFAULT fallback). Returns the resolved profile name and whether it is a silent fallback. `loadFromConfig` no longer needs to know about `__settings__` at all, replacing two booleans (`hasExplicitProfile`, `hasDefaultProfileSetting`) with a single `isFallback` flag. Using `__settings__` as a profile name (either via `--profile` or `default_profile`) now returns a specific error ("reserved section name") instead of the generic "has no profile configured". **`DefaultProfileTest.java`**: 8 test scenarios covering resolution, precedence, legacy fallback, empty settings, explicit override, settings-not-a-profile (both via default_profile and explicit --profile), and nonexistent profile error. ## Test plan - [x] `default_profile` resolves correctly - [x] `default_profile` takes precedence over `[DEFAULT]` - [x] Legacy fallback when no `[__settings__]` - [x] Legacy fallback when `default_profile` is empty - [x] `[__settings__]` is not treated as a profile (via `default_profile`) - [x] `[__settings__]` is not treated as a profile (via explicit `--profile`) - [x] Explicit `--profile` overrides `default_profile` - [x] `default_profile` pointing to nonexistent section throws error --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7175953 commit bfb25d4

File tree

11 files changed

+354
-104
lines changed

11 files changed

+354
-104
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.104.0
44

55
### New Features and Improvements
6+
* Support `default_profile` in `[__settings__]` section of `.databrickscfg` for consistent default profile resolution across CLI and SDKs.
67
* Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment.
78

89
### Bug Fixes

databricks-sdk-java/lockfile.json

Lines changed: 92 additions & 92 deletions
Large diffs are not rendered by default.

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

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
@InternalApi
2020
public class ConfigLoader {
2121
private static final Logger LOG = LoggerFactory.getLogger(ConfigLoader.class);
22+
private static final String SETTINGS_SECTION = "__settings__";
2223

2324
private static final List<ConfigAttributeAccessor> accessors = attributeAccessors();
2425

@@ -92,22 +93,25 @@ static void loadFromConfig(DatabricksConfig cfg) throws IllegalAccessException {
9293
INIConfiguration ini = parseDatabricksCfg(configFile, isDefaultConfig);
9394
if (ini == null) return;
9495

95-
String profile = cfg.getProfile();
96-
boolean hasExplicitProfile = !isNullOrEmpty(profile);
97-
if (!hasExplicitProfile) {
98-
profile = "DEFAULT";
99-
}
96+
ResolvedProfile resolved = resolveProfile(cfg.getProfile(), ini, configFile.toString());
97+
String profile = resolved.name;
98+
boolean isFallback = resolved.isFallback;
99+
100100
SubnodeConfiguration section = ini.getSection(profile);
101101
boolean sectionNotPresent = section == null || section.isEmpty();
102-
if (sectionNotPresent && !hasExplicitProfile) {
103-
LOG.info("{} has no {} profile configured", configFile, profile);
104-
return;
105-
}
106102
if (sectionNotPresent) {
103+
if (isFallback) {
104+
LOG.info("{} has no {} profile configured", configFile, profile);
105+
return;
106+
}
107107
String msg = String.format("resolve: %s has no %s profile configured", configFile, profile);
108108
throw new DatabricksException(msg);
109109
}
110110

111+
if (!isFallback) {
112+
cfg.setProfile(profile);
113+
}
114+
111115
for (ConfigAttributeAccessor accessor : accessors) {
112116
String value = section.getString(accessor.getName());
113117
if (!isNullOrEmpty(accessor.getValueFromConfig(cfg))) {
@@ -117,6 +121,61 @@ static void loadFromConfig(DatabricksConfig cfg) throws IllegalAccessException {
117121
}
118122
}
119123

124+
static class ResolvedProfile {
125+
final String name;
126+
final boolean isFallback;
127+
128+
ResolvedProfile(String name, boolean isFallback) {
129+
this.name = name;
130+
this.isFallback = isFallback;
131+
}
132+
}
133+
134+
/**
135+
* Resolves which profile to use from the config file.
136+
*
137+
* <p>Resolution order:
138+
*
139+
* <ol>
140+
* <li>Explicit profile (flag, env var, or programmatic config) with isFallback=false
141+
* <li>{@code [__settings__].default_profile} with isFallback=false
142+
* <li>{@code "DEFAULT"} with isFallback=true
143+
* </ol>
144+
*
145+
* @throws DatabricksException if the resolved profile is the reserved __settings__ section
146+
*/
147+
static ResolvedProfile resolveProfile(
148+
String requestedProfile, INIConfiguration ini, String configFile) {
149+
if (!isNullOrEmpty(requestedProfile)) {
150+
if (SETTINGS_SECTION.equals(requestedProfile)) {
151+
throw new DatabricksException(
152+
String.format(
153+
"%s: %s is a reserved section name and cannot be used as a profile",
154+
configFile, SETTINGS_SECTION));
155+
}
156+
return new ResolvedProfile(requestedProfile, false);
157+
}
158+
159+
SubnodeConfiguration settings = ini.getSection(SETTINGS_SECTION);
160+
if (settings != null && !settings.isEmpty()) {
161+
String defaultProfile = settings.getString("default_profile");
162+
if (defaultProfile != null) {
163+
defaultProfile = defaultProfile.trim();
164+
}
165+
if (!isNullOrEmpty(defaultProfile)) {
166+
if (SETTINGS_SECTION.equals(defaultProfile)) {
167+
throw new DatabricksException(
168+
String.format(
169+
"%s: %s is a reserved section name and cannot be used as a profile",
170+
configFile, SETTINGS_SECTION));
171+
}
172+
return new ResolvedProfile(defaultProfile, false);
173+
}
174+
}
175+
176+
return new ResolvedProfile("DEFAULT", true);
177+
}
178+
120179
private static INIConfiguration parseDatabricksCfg(String configFile, boolean isDefaultConfig) {
121180
INIConfiguration iniConfig = new INIConfiguration();
122181
try (FileReader reader = new FileReader(configFile)) {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.databricks.sdk;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
import static org.mockito.Mockito.mock;
7+
8+
import com.databricks.sdk.core.ConfigResolving;
9+
import com.databricks.sdk.core.DatabricksConfig;
10+
import com.databricks.sdk.core.DatabricksException;
11+
import com.databricks.sdk.core.http.HttpClient;
12+
import com.databricks.sdk.core.utils.TestOSUtils;
13+
import org.junit.jupiter.api.Test;
14+
15+
public class DefaultProfileTest implements ConfigResolving {
16+
17+
private DatabricksConfig createConfigWithMockClient() {
18+
HttpClient mockClient = mock(HttpClient.class);
19+
return new DatabricksConfig().setHttpClient(mockClient);
20+
}
21+
22+
/** Test 1: default_profile resolves correctly and is written back to config */
23+
@Test
24+
public void testDefaultProfileResolvesCorrectly() {
25+
StaticEnv env = new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile"));
26+
DatabricksConfig config = createConfigWithMockClient();
27+
resolveConfig(config, env);
28+
config.authenticate();
29+
30+
assertEquals("pat", config.getAuthType());
31+
assertEquals("https://my-workspace.cloud.databricks.com", config.getHost());
32+
assertEquals("my-workspace", config.getProfile());
33+
}
34+
35+
/** Test 2: default_profile takes precedence over [DEFAULT] */
36+
@Test
37+
public void testDefaultProfileTakesPrecedenceOverDefault() {
38+
StaticEnv env =
39+
new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile_precedence"));
40+
DatabricksConfig config = createConfigWithMockClient();
41+
resolveConfig(config, env);
42+
config.authenticate();
43+
44+
assertEquals("pat", config.getAuthType());
45+
assertEquals("https://my-workspace.cloud.databricks.com", config.getHost());
46+
}
47+
48+
/** Test 3: Legacy fallback when no [__settings__] */
49+
@Test
50+
public void testLegacyFallbackWhenNoSettings() {
51+
StaticEnv env = new StaticEnv().with("HOME", TestOSUtils.resource("/testdata"));
52+
DatabricksConfig config = createConfigWithMockClient();
53+
resolveConfig(config, env);
54+
config.authenticate();
55+
56+
assertEquals("pat", config.getAuthType());
57+
assertEquals("https://dbc-XXXXXXXX-YYYY.cloud.databricks.com", config.getHost());
58+
}
59+
60+
/** Test 4: Legacy fallback when default_profile is empty */
61+
@Test
62+
public void testLegacyFallbackWhenDefaultProfileEmpty() {
63+
StaticEnv env =
64+
new StaticEnv()
65+
.with("HOME", TestOSUtils.resource("/testdata/default_profile_empty_settings"));
66+
DatabricksConfig config = createConfigWithMockClient();
67+
resolveConfig(config, env);
68+
config.authenticate();
69+
70+
assertEquals("pat", config.getAuthType());
71+
assertEquals("https://default.cloud.databricks.com", config.getHost());
72+
}
73+
74+
/** Test 5: default_profile = __settings__ is rejected */
75+
@Test
76+
public void testSettingsSelfReferenceIsRejected() {
77+
StaticEnv env =
78+
new StaticEnv()
79+
.with("HOME", TestOSUtils.resource("/testdata/default_profile_settings_self_ref"));
80+
DatabricksConfig config = createConfigWithMockClient();
81+
82+
DatabricksException ex =
83+
assertThrows(
84+
DatabricksException.class,
85+
() -> {
86+
resolveConfig(config, env);
87+
config.authenticate();
88+
});
89+
assertTrue(
90+
ex.getMessage().contains("reserved section name"),
91+
"Error should reject __settings__ as a profile target: " + ex.getMessage());
92+
}
93+
94+
/** Test 6: Explicit --profile overrides default_profile */
95+
@Test
96+
public void testExplicitProfileOverridesDefaultProfile() {
97+
StaticEnv env =
98+
new StaticEnv()
99+
.with("DATABRICKS_CONFIG_PROFILE", "other")
100+
.with("HOME", TestOSUtils.resource("/testdata/default_profile_explicit_override"));
101+
DatabricksConfig config = createConfigWithMockClient();
102+
resolveConfig(config, env);
103+
config.authenticate();
104+
105+
assertEquals("pat", config.getAuthType());
106+
assertEquals("https://other.cloud.databricks.com", config.getHost());
107+
}
108+
109+
@Test
110+
public void testExplicitSettingsSectionProfileIsRejected() {
111+
StaticEnv env =
112+
new StaticEnv()
113+
.with("DATABRICKS_CONFIG_PROFILE", "__settings__")
114+
.with("HOME", TestOSUtils.resource("/testdata/default_profile"));
115+
DatabricksConfig config = createConfigWithMockClient();
116+
117+
DatabricksException ex =
118+
assertThrows(
119+
DatabricksException.class,
120+
() -> {
121+
resolveConfig(config, env);
122+
config.authenticate();
123+
});
124+
assertTrue(
125+
ex.getMessage().contains("reserved section name"),
126+
"Error should reject __settings__ as a profile target: " + ex.getMessage());
127+
}
128+
129+
/** Test 7: default_profile pointing to nonexistent section */
130+
@Test
131+
public void testDefaultProfileNonexistentSection() {
132+
StaticEnv env =
133+
new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile_nonexistent"));
134+
DatabricksConfig config = createConfigWithMockClient();
135+
136+
DatabricksException ex =
137+
assertThrows(
138+
DatabricksException.class,
139+
() -> {
140+
resolveConfig(config, env);
141+
config.authenticate();
142+
});
143+
assertTrue(
144+
ex.getMessage().contains("deleted-profile"),
145+
"Error should mention the missing profile name: " + ex.getMessage());
146+
}
147+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[__settings__]
2+
default_profile = my-workspace
3+
4+
[my-workspace]
5+
host = https://my-workspace.cloud.databricks.com
6+
token = dapiXYZ
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[__settings__]
2+
3+
[DEFAULT]
4+
host = https://default.cloud.databricks.com
5+
token = dapiXYZ
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[__settings__]
2+
default_profile = my-workspace
3+
4+
[my-workspace]
5+
host = https://my-workspace.cloud.databricks.com
6+
token = dapiXYZ
7+
8+
[other]
9+
host = https://other.cloud.databricks.com
10+
token = dapiOTHER
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[__settings__]
2+
default_profile = deleted-profile
3+
4+
[my-workspace]
5+
host = https://my-workspace.cloud.databricks.com
6+
token = dapiXYZ
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[__settings__]
2+
default_profile = my-workspace
3+
4+
[DEFAULT]
5+
host = https://default.cloud.databricks.com
6+
token = dapiOLD
7+
8+
[my-workspace]
9+
host = https://my-workspace.cloud.databricks.com
10+
token = dapiXYZ
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[__settings__]
2+
default_profile = __settings__
3+
4+
[DEFAULT]
5+
host = https://default.cloud.databricks.com
6+
token = dapiXYZ

0 commit comments

Comments
 (0)