Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Release v0.104.0

### New Features and Improvements
* Support `default_profile` in `[__settings__]` section of `.databrickscfg` for consistent default profile resolution across CLI and SDKs.
* 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.

### Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@InternalApi
public class ConfigLoader {
private static final Logger LOG = LoggerFactory.getLogger(ConfigLoader.class);
private static final String SETTINGS_SECTION = "__settings__";

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

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

String profile = cfg.getProfile();
boolean hasExplicitProfile = !isNullOrEmpty(profile);
if (!hasExplicitProfile) {
profile = "DEFAULT";
}
ResolvedProfile resolved = resolveProfile(cfg.getProfile(), ini, configFile.toString());
String profile = resolved.name;
boolean isFallback = resolved.isFallback;

SubnodeConfiguration section = ini.getSection(profile);
boolean sectionNotPresent = section == null || section.isEmpty();
if (sectionNotPresent && !hasExplicitProfile) {
LOG.info("{} has no {} profile configured", configFile, profile);
return;
}
if (sectionNotPresent) {
if (isFallback) {
LOG.info("{} has no {} profile configured", configFile, profile);
return;
}
String msg = String.format("resolve: %s has no %s profile configured", configFile, profile);
throw new DatabricksException(msg);
}

if (!isFallback) {
cfg.setProfile(profile);
}

for (ConfigAttributeAccessor accessor : accessors) {
String value = section.getString(accessor.getName());
if (!isNullOrEmpty(accessor.getValueFromConfig(cfg))) {
Expand All @@ -117,6 +121,61 @@ static void loadFromConfig(DatabricksConfig cfg) throws IllegalAccessException {
}
}

static class ResolvedProfile {
final String name;
final boolean isFallback;

ResolvedProfile(String name, boolean isFallback) {
this.name = name;
this.isFallback = isFallback;
}
}

/**
* Resolves which profile to use from the config file.
*
* <p>Resolution order:
*
* <ol>
* <li>Explicit profile (flag, env var, or programmatic config) with isFallback=false
* <li>{@code [__settings__].default_profile} with isFallback=false
* <li>{@code "DEFAULT"} with isFallback=true
* </ol>
*
* @throws DatabricksException if the resolved profile is the reserved __settings__ section
*/
static ResolvedProfile resolveProfile(
String requestedProfile, INIConfiguration ini, String configFile) {
if (!isNullOrEmpty(requestedProfile)) {
if (SETTINGS_SECTION.equals(requestedProfile)) {
throw new DatabricksException(
String.format(
"%s: %s is a reserved section name and cannot be used as a profile",
configFile, SETTINGS_SECTION));
}
return new ResolvedProfile(requestedProfile, false);
}

SubnodeConfiguration settings = ini.getSection(SETTINGS_SECTION);
if (settings != null && !settings.isEmpty()) {
String defaultProfile = settings.getString("default_profile");
if (defaultProfile != null) {
defaultProfile = defaultProfile.trim();
}
if (!isNullOrEmpty(defaultProfile)) {
if (SETTINGS_SECTION.equals(defaultProfile)) {
throw new DatabricksException(
String.format(
"%s: %s is a reserved section name and cannot be used as a profile",
configFile, SETTINGS_SECTION));
}
return new ResolvedProfile(defaultProfile, false);
}
}

return new ResolvedProfile("DEFAULT", true);
}

private static INIConfiguration parseDatabricksCfg(String configFile, boolean isDefaultConfig) {
INIConfiguration iniConfig = new INIConfiguration();
try (FileReader reader = new FileReader(configFile)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.databricks.sdk;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;

import com.databricks.sdk.core.ConfigResolving;
import com.databricks.sdk.core.DatabricksConfig;
import com.databricks.sdk.core.DatabricksException;
import com.databricks.sdk.core.http.HttpClient;
import com.databricks.sdk.core.utils.TestOSUtils;
import org.junit.jupiter.api.Test;

public class DefaultProfileTest implements ConfigResolving {

private DatabricksConfig createConfigWithMockClient() {
HttpClient mockClient = mock(HttpClient.class);
return new DatabricksConfig().setHttpClient(mockClient);
}

/** Test 1: default_profile resolves correctly and is written back to config */
@Test
public void testDefaultProfileResolvesCorrectly() {
StaticEnv env = new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile"));
DatabricksConfig config = createConfigWithMockClient();
resolveConfig(config, env);
config.authenticate();

assertEquals("pat", config.getAuthType());
assertEquals("https://my-workspace.cloud.databricks.com", config.getHost());
assertEquals("my-workspace", config.getProfile());
}

/** Test 2: default_profile takes precedence over [DEFAULT] */
@Test
public void testDefaultProfileTakesPrecedenceOverDefault() {
StaticEnv env =
new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile_precedence"));
DatabricksConfig config = createConfigWithMockClient();
resolveConfig(config, env);
config.authenticate();

assertEquals("pat", config.getAuthType());
assertEquals("https://my-workspace.cloud.databricks.com", config.getHost());
}

/** Test 3: Legacy fallback when no [__settings__] */
@Test
public void testLegacyFallbackWhenNoSettings() {
StaticEnv env = new StaticEnv().with("HOME", TestOSUtils.resource("/testdata"));
DatabricksConfig config = createConfigWithMockClient();
resolveConfig(config, env);
config.authenticate();

assertEquals("pat", config.getAuthType());
assertEquals("https://dbc-XXXXXXXX-YYYY.cloud.databricks.com", config.getHost());
}

/** Test 4: Legacy fallback when default_profile is empty */
@Test
public void testLegacyFallbackWhenDefaultProfileEmpty() {
StaticEnv env =
new StaticEnv()
.with("HOME", TestOSUtils.resource("/testdata/default_profile_empty_settings"));
DatabricksConfig config = createConfigWithMockClient();
resolveConfig(config, env);
config.authenticate();

assertEquals("pat", config.getAuthType());
assertEquals("https://default.cloud.databricks.com", config.getHost());
}

/** Test 5: default_profile = __settings__ is rejected */
@Test
public void testSettingsSelfReferenceIsRejected() {
StaticEnv env =
new StaticEnv()
.with("HOME", TestOSUtils.resource("/testdata/default_profile_settings_self_ref"));
DatabricksConfig config = createConfigWithMockClient();

DatabricksException ex =
assertThrows(
DatabricksException.class,
() -> {
resolveConfig(config, env);
config.authenticate();
});
assertTrue(
ex.getMessage().contains("reserved section name"),
"Error should reject __settings__ as a profile target: " + ex.getMessage());
}

/** Test 6: Explicit --profile overrides default_profile */
@Test
public void testExplicitProfileOverridesDefaultProfile() {
StaticEnv env =
new StaticEnv()
.with("DATABRICKS_CONFIG_PROFILE", "other")
.with("HOME", TestOSUtils.resource("/testdata/default_profile_explicit_override"));
DatabricksConfig config = createConfigWithMockClient();
resolveConfig(config, env);
config.authenticate();

assertEquals("pat", config.getAuthType());
assertEquals("https://other.cloud.databricks.com", config.getHost());
}

@Test
public void testExplicitSettingsSectionProfileIsRejected() {
StaticEnv env =
new StaticEnv()
.with("DATABRICKS_CONFIG_PROFILE", "__settings__")
.with("HOME", TestOSUtils.resource("/testdata/default_profile"));
DatabricksConfig config = createConfigWithMockClient();

DatabricksException ex =
assertThrows(
DatabricksException.class,
() -> {
resolveConfig(config, env);
config.authenticate();
});
assertTrue(
ex.getMessage().contains("reserved section name"),
"Error should reject __settings__ as a profile target: " + ex.getMessage());
}

/** Test 7: default_profile pointing to nonexistent section */
@Test
public void testDefaultProfileNonexistentSection() {
StaticEnv env =
new StaticEnv().with("HOME", TestOSUtils.resource("/testdata/default_profile_nonexistent"));
DatabricksConfig config = createConfigWithMockClient();

DatabricksException ex =
assertThrows(
DatabricksException.class,
() -> {
resolveConfig(config, env);
config.authenticate();
});
assertTrue(
ex.getMessage().contains("deleted-profile"),
"Error should mention the missing profile name: " + ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[__settings__]
default_profile = my-workspace

[my-workspace]
host = https://my-workspace.cloud.databricks.com
token = dapiXYZ
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[__settings__]

[DEFAULT]
host = https://default.cloud.databricks.com
token = dapiXYZ
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[__settings__]
default_profile = my-workspace

[my-workspace]
host = https://my-workspace.cloud.databricks.com
token = dapiXYZ

[other]
host = https://other.cloud.databricks.com
token = dapiOTHER
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[__settings__]
default_profile = deleted-profile

[my-workspace]
host = https://my-workspace.cloud.databricks.com
token = dapiXYZ
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[__settings__]
default_profile = my-workspace

[DEFAULT]
host = https://default.cloud.databricks.com
token = dapiOLD

[my-workspace]
host = https://my-workspace.cloud.databricks.com
token = dapiXYZ
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[__settings__]
default_profile = __settings__

[DEFAULT]
host = https://default.cloud.databricks.com
token = dapiXYZ
Loading