diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 3d39694b1..20b06b208 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -3,6 +3,7 @@ ## Release v0.100.0 ### New Features and Improvements +* Support `default_profile` in `[__settings__]` section of `.databrickscfg` for consistent default profile resolution across CLI and SDKs. ### Bug Fixes diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java index ae531ffc0..453b87608 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java @@ -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 accessors = attributeAccessors(); @@ -94,12 +95,27 @@ static void loadFromConfig(DatabricksConfig cfg) throws IllegalAccessException { String profile = cfg.getProfile(); boolean hasExplicitProfile = !isNullOrEmpty(profile); + boolean hasDefaultProfileSetting = false; if (!hasExplicitProfile) { + 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)) { + profile = defaultProfile; + hasDefaultProfileSetting = true; + } + } + } + if (!hasExplicitProfile && !hasDefaultProfileSetting) { profile = "DEFAULT"; } - SubnodeConfiguration section = ini.getSection(profile); + SubnodeConfiguration section = + SETTINGS_SECTION.equals(profile) ? null : ini.getSection(profile); boolean sectionNotPresent = section == null || section.isEmpty(); - if (sectionNotPresent && !hasExplicitProfile) { + if (sectionNotPresent && !hasExplicitProfile && !hasDefaultProfileSetting) { LOG.info("{} has no {} profile configured", configFile, profile); return; } @@ -107,6 +123,9 @@ static void loadFromConfig(DatabricksConfig cfg) throws IllegalAccessException { String msg = String.format("resolve: %s has no %s profile configured", configFile, profile); throw new DatabricksException(msg); } + if (hasDefaultProfileSetting) { + cfg.setProfile(profile); + } for (ConfigAttributeAccessor accessor : accessors) { String value = section.getString(accessor.getName()); diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/DefaultProfileTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/DefaultProfileTest.java new file mode 100644 index 000000000..df5937bac --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/DefaultProfileTest.java @@ -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("has no __settings__ profile configured"), + "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("has no __settings__ profile configured"), + "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()); + } +} diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile/.databrickscfg new file mode 100644 index 000000000..9a3f6a2d8 --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile/.databrickscfg @@ -0,0 +1,6 @@ +[__settings__] +default_profile = my-workspace + +[my-workspace] +host = https://my-workspace.cloud.databricks.com +token = dapiXYZ diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile_empty_settings/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile_empty_settings/.databrickscfg new file mode 100644 index 000000000..46880f70e --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile_empty_settings/.databrickscfg @@ -0,0 +1,5 @@ +[__settings__] + +[DEFAULT] +host = https://default.cloud.databricks.com +token = dapiXYZ diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile_explicit_override/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile_explicit_override/.databrickscfg new file mode 100644 index 000000000..c3f9f79be --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile_explicit_override/.databrickscfg @@ -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 diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile_nonexistent/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile_nonexistent/.databrickscfg new file mode 100644 index 000000000..d2c144c92 --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile_nonexistent/.databrickscfg @@ -0,0 +1,6 @@ +[__settings__] +default_profile = deleted-profile + +[my-workspace] +host = https://my-workspace.cloud.databricks.com +token = dapiXYZ diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile_precedence/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile_precedence/.databrickscfg new file mode 100644 index 000000000..ac8f295d6 --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile_precedence/.databrickscfg @@ -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 diff --git a/databricks-sdk-java/src/test/resources/testdata/default_profile_settings_self_ref/.databrickscfg b/databricks-sdk-java/src/test/resources/testdata/default_profile_settings_self_ref/.databrickscfg new file mode 100644 index 000000000..05f711147 --- /dev/null +++ b/databricks-sdk-java/src/test/resources/testdata/default_profile_settings_self_ref/.databrickscfg @@ -0,0 +1,6 @@ +[__settings__] +default_profile = __settings__ + +[DEFAULT] +host = https://default.cloud.databricks.com +token = dapiXYZ