Skip to content

Commit 0113ee1

Browse files
alyssa-dbshreyas-goenkaandrewnester
authored
Preserve custom profile fields during 'databricks auth login' (#1897) (#2988)
### Changes - Fixed an issue where running `databricks auth login` would remove the `cluster_id` field from profiles in `.databrickscfg`. The login process now preserves the `cluster_id` field. - Added a test to ensure `cluster_id` is retained after login. Fixes #1897 ## Tests Added `TestLoginPreservesClusterID`, which tests a new example profile named `cluster-profile` in `.databrickscfg` to see if the `cluster_id` is properly fetched from the config file path. ### Reproduction Steps 1. Create a profile with extra fields (e.g., `cluster_id`) in `.databrickscfg`. 2. Run `databricks auth login`. 3. Previously, `cluster_id` would be deleted. Now, it is preserved. --------- Co-authored-by: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Co-authored-by: Andrew Nester <andrew.nester.dev@gmail.com>
1 parent c4632f6 commit 0113ee1

File tree

7 files changed

+171
-24
lines changed

7 files changed

+171
-24
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Dependency updates
88

99
### CLI
10+
* Fixed an issue where running `databricks auth login` would remove the `cluster_id` field from profiles in `.databrickscfg`. The login process now preserves the `cluster_id` field. ([#2988](https://github.com/databricks/cli/pull/2988))
1011

1112
### Bundles
1213

cmd/auth/login.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,22 @@ depends on the existing profiles you have set in your configuration file
106106
}
107107
}
108108

109+
existingProfile, err := loadProfileByName(ctx, profileName, profile.DefaultProfiler)
110+
if err != nil {
111+
return err
112+
}
113+
109114
// Set the host and account-id based on the provided arguments and flags.
110-
err := setHostAndAccountId(ctx, profile.DefaultProfiler, profileName, authArguments, args)
115+
err = setHostAndAccountId(ctx, existingProfile, authArguments, args)
111116
if err != nil {
112117
return err
113118
}
119+
120+
clusterID := ""
121+
if existingProfile != nil {
122+
clusterID = existingProfile.ClusterID
123+
}
124+
114125
oauthArgument, err := authArguments.ToOAuthArgument()
115126
if err != nil {
116127
return err
@@ -127,6 +138,7 @@ depends on the existing profiles you have set in your configuration file
127138
Host: authArguments.Host,
128139
AccountID: authArguments.AccountID,
129140
AuthType: "databricks-cli",
141+
ClusterID: clusterID,
130142
}
131143

132144
ctx, cancel := context.WithTimeout(ctx, loginTimeout)
@@ -182,27 +194,21 @@ depends on the existing profiles you have set in your configuration file
182194
// 1. --account-id flag.
183195
// 2. account-id from the specified profile, if available.
184196
// 3. Prompt the user for the account-id.
185-
func setHostAndAccountId(ctx context.Context, profiler profile.Profiler, profileName string, authArguments *auth.AuthArguments, args []string) error {
197+
func setHostAndAccountId(ctx context.Context, existingProfile *profile.Profile, authArguments *auth.AuthArguments, args []string) error {
186198
// If both [HOST] and --host are provided, return an error.
187199
host := authArguments.Host
188200
if len(args) > 0 && host != "" {
189201
return errors.New("please only provide a host as an argument or a flag, not both")
190202
}
191203

192204
// If the chosen profile has a hostname and the user hasn't specified a host, infer the host from the profile.
193-
profiles, err := profiler.LoadProfiles(ctx, profile.WithName(profileName))
194-
// Tolerate ErrNoConfiguration here, as we will write out a configuration as part of the login flow.
195-
if err != nil && !errors.Is(err, profile.ErrNoConfiguration) {
196-
return err
197-
}
198-
199205
if host == "" {
200206
if len(args) > 0 {
201207
// If [HOST] is provided, set the host to the provided positional argument.
202208
authArguments.Host = args[0]
203-
} else if len(profiles) > 0 && profiles[0].Host != "" {
209+
} else if existingProfile != nil && existingProfile.Host != "" {
204210
// If neither [HOST] nor --host are provided, and the profile has a host, use it.
205-
authArguments.Host = profiles[0].Host
211+
authArguments.Host = existingProfile.Host
206212
} else {
207213
// If neither [HOST] nor --host are provided, and the profile does not have a host,
208214
// then prompt the user for a host.
@@ -219,8 +225,8 @@ func setHostAndAccountId(ctx context.Context, profiler profile.Profiler, profile
219225
isAccountClient := (&config.Config{Host: authArguments.Host}).IsAccountClient()
220226
accountID := authArguments.AccountID
221227
if isAccountClient && accountID == "" {
222-
if len(profiles) > 0 && profiles[0].AccountID != "" {
223-
authArguments.AccountID = profiles[0].AccountID
228+
if existingProfile != nil && existingProfile.AccountID != "" {
229+
authArguments.AccountID = existingProfile.AccountID
224230
} else {
225231
// Prompt user for the account-id if it we could not get it from a
226232
// profile.
@@ -245,3 +251,25 @@ func getProfileName(authArguments *auth.AuthArguments) string {
245251
split := strings.Split(host, ".")
246252
return split[0]
247253
}
254+
255+
func loadProfileByName(ctx context.Context, profileName string, profiler profile.Profiler) (*profile.Profile, error) {
256+
if profileName == "" {
257+
return nil, nil
258+
}
259+
260+
if profiler == nil {
261+
return nil, errors.New("profiler cannot be nil")
262+
}
263+
264+
profiles, err := profiler.LoadProfiles(ctx, profile.WithName(profileName))
265+
// Tolerate ErrNoConfiguration here, as we will write out a configuration as part of the login flow.
266+
if err != nil && !errors.Is(err, profile.ErrNoConfiguration) {
267+
return nil, err
268+
}
269+
270+
if len(profiles) > 0 {
271+
// LoadProfiles returns only one profile per name, even with multiple profiles in the config file with the same name.
272+
return &profiles[0], nil
273+
}
274+
return nil, nil
275+
}

cmd/auth/login_test.go

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,64 @@ import (
1212
"github.com/stretchr/testify/require"
1313
)
1414

15+
func loadTestProfile(t *testing.T, ctx context.Context, profileName string) *profile.Profile {
16+
profile, err := loadProfileByName(ctx, profileName, profile.DefaultProfiler)
17+
require.NoError(t, err)
18+
require.NotNil(t, profile)
19+
return profile
20+
}
21+
1522
func TestSetHostDoesNotFailWithNoDatabrickscfg(t *testing.T) {
1623
ctx := context.Background()
1724
ctx = env.Set(ctx, "DATABRICKS_CONFIG_FILE", "./imaginary-file/databrickscfg")
18-
err := setHostAndAccountId(ctx, profile.DefaultProfiler, "foo", &auth.AuthArguments{Host: "test"}, []string{})
25+
26+
existingProfile, err := loadProfileByName(ctx, "foo", profile.DefaultProfiler)
27+
assert.NoError(t, err)
28+
29+
err = setHostAndAccountId(ctx, existingProfile, &auth.AuthArguments{Host: "test"}, []string{})
1930
assert.NoError(t, err)
2031
}
2132

2233
func TestSetHost(t *testing.T) {
23-
authArguments := auth.AuthArguments{}
34+
var authArguments auth.AuthArguments
2435
t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg")
2536
ctx, _ := cmdio.SetupTest(context.Background())
2637

38+
profile1 := loadTestProfile(t, ctx, "profile-1")
39+
profile2 := loadTestProfile(t, ctx, "profile-2")
40+
2741
// Test error when both flag and argument are provided
2842
authArguments.Host = "val from --host"
29-
err := setHostAndAccountId(ctx, profile.DefaultProfiler, "profile-1", &authArguments, []string{"val from [HOST]"})
43+
err := setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"})
3044
assert.EqualError(t, err, "please only provide a host as an argument or a flag, not both")
3145

3246
// Test setting host from flag
3347
authArguments.Host = "val from --host"
34-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "profile-1", &authArguments, []string{})
48+
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{})
3549
assert.NoError(t, err)
3650
assert.Equal(t, "val from --host", authArguments.Host)
3751

3852
// Test setting host from argument
3953
authArguments.Host = ""
40-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "profile-1", &authArguments, []string{"val from [HOST]"})
54+
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{"val from [HOST]"})
4155
assert.NoError(t, err)
4256
assert.Equal(t, "val from [HOST]", authArguments.Host)
4357

4458
// Test setting host from profile
4559
authArguments.Host = ""
46-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "profile-1", &authArguments, []string{})
60+
err = setHostAndAccountId(ctx, profile1, &authArguments, []string{})
4761
assert.NoError(t, err)
4862
assert.Equal(t, "https://www.host1.com", authArguments.Host)
4963

5064
// Test setting host from profile
5165
authArguments.Host = ""
52-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "profile-2", &authArguments, []string{})
66+
err = setHostAndAccountId(ctx, profile2, &authArguments, []string{})
5367
assert.NoError(t, err)
5468
assert.Equal(t, "https://www.host2.com", authArguments.Host)
5569

5670
// Test host is not set. Should prompt.
5771
authArguments.Host = ""
58-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "", &authArguments, []string{})
72+
err = setHostAndAccountId(ctx, nil, &authArguments, []string{})
5973
assert.EqualError(t, err, "the command is being run in a non-interactive environment, please specify a host using --host")
6074
}
6175

@@ -64,23 +78,112 @@ func TestSetAccountId(t *testing.T) {
6478
t.Setenv("DATABRICKS_CONFIG_FILE", "./testdata/.databrickscfg")
6579
ctx, _ := cmdio.SetupTest(context.Background())
6680

81+
accountProfile := loadTestProfile(t, ctx, "account-profile")
82+
6783
// Test setting account-id from flag
6884
authArguments.AccountID = "val from --account-id"
69-
err := setHostAndAccountId(ctx, profile.DefaultProfiler, "account-profile", &authArguments, []string{})
85+
err := setHostAndAccountId(ctx, accountProfile, &authArguments, []string{})
7086
assert.NoError(t, err)
7187
assert.Equal(t, "https://accounts.cloud.databricks.com", authArguments.Host)
7288
assert.Equal(t, "val from --account-id", authArguments.AccountID)
7389

7490
// Test setting account_id from profile
7591
authArguments.AccountID = ""
76-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "account-profile", &authArguments, []string{})
92+
err = setHostAndAccountId(ctx, accountProfile, &authArguments, []string{})
7793
require.NoError(t, err)
7894
assert.Equal(t, "https://accounts.cloud.databricks.com", authArguments.Host)
7995
assert.Equal(t, "id-from-profile", authArguments.AccountID)
8096

8197
// Neither flag nor profile account-id is set, should prompt
8298
authArguments.AccountID = ""
8399
authArguments.Host = "https://accounts.cloud.databricks.com"
84-
err = setHostAndAccountId(ctx, profile.DefaultProfiler, "", &authArguments, []string{})
100+
err = setHostAndAccountId(ctx, nil, &authArguments, []string{})
85101
assert.EqualError(t, err, "the command is being run in a non-interactive environment, please specify an account ID using --account-id")
86102
}
103+
104+
func TestLoadProfileByNameAndClusterID(t *testing.T) {
105+
testCases := []struct {
106+
name string
107+
profile string
108+
configFileEnv string
109+
homeDirOverride string
110+
expectedHost string
111+
expectedClusterID string
112+
}{
113+
{
114+
name: "cluster profile",
115+
profile: "cluster-profile",
116+
configFileEnv: "./testdata/.databrickscfg",
117+
expectedHost: "https://www.host2.com",
118+
expectedClusterID: "cluster-from-config",
119+
},
120+
{
121+
name: "profile from home directory (existing)",
122+
profile: "cluster-profile",
123+
homeDirOverride: "testdata",
124+
expectedHost: "https://www.host2.com",
125+
expectedClusterID: "cluster-from-config",
126+
},
127+
{
128+
name: "profile does not exist",
129+
profile: "no-profile",
130+
configFileEnv: "./testdata/.databrickscfg",
131+
expectedHost: "",
132+
expectedClusterID: "",
133+
},
134+
{
135+
name: "account profile",
136+
profile: "account-profile",
137+
configFileEnv: "./testdata/.databrickscfg",
138+
expectedHost: "https://accounts.cloud.databricks.com",
139+
expectedClusterID: "",
140+
},
141+
{
142+
name: "config doesn't exist",
143+
profile: "any-profile",
144+
configFileEnv: "./nonexistent/.databrickscfg",
145+
expectedHost: "",
146+
expectedClusterID: "",
147+
},
148+
{
149+
name: "profile from home directory (non-existent)",
150+
profile: "any-profile",
151+
homeDirOverride: "nonexistent",
152+
expectedHost: "",
153+
expectedClusterID: "",
154+
},
155+
{
156+
name: "invalid profile (missing host)",
157+
profile: "invalid-profile",
158+
configFileEnv: "./testdata/.databrickscfg",
159+
expectedHost: "",
160+
expectedClusterID: "",
161+
},
162+
}
163+
164+
for _, tc := range testCases {
165+
t.Run(tc.name, func(t *testing.T) {
166+
ctx := context.Background()
167+
168+
if tc.configFileEnv != "" {
169+
t.Setenv("DATABRICKS_CONFIG_FILE", tc.configFileEnv)
170+
} else if tc.homeDirOverride != "" {
171+
// Use default ~/.databrickscfg
172+
ctx = env.WithUserHomeDir(ctx, tc.homeDirOverride)
173+
}
174+
175+
profile, err := loadProfileByName(ctx, tc.profile, profile.DefaultProfiler)
176+
require.NoError(t, err)
177+
178+
if tc.expectedHost == "" {
179+
assert.Nil(t, profile, "Test case '%s' failed: expected nil profile but got non-nil profile", tc.name)
180+
} else {
181+
assert.NotNil(t, profile, "Test case '%s' failed: expected profile but got nil", tc.name)
182+
assert.Equal(t, tc.expectedHost, profile.Host,
183+
"Test case '%s' failed: expected host '%s', but got '%s'", tc.name, tc.expectedHost, profile.Host)
184+
assert.Equal(t, tc.expectedClusterID, profile.ClusterID,
185+
"Test case '%s' failed: expected cluster ID '%s', but got '%s'", tc.name, tc.expectedClusterID, profile.ClusterID)
186+
}
187+
})
188+
}
189+
}

cmd/auth/testdata/.databrickscfg

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ host = https://www.host2.com
77
[account-profile]
88
host = https://accounts.cloud.databricks.com
99
account_id = id-from-profile
10+
11+
[cluster-profile]
12+
host = https://www.host2.com
13+
cluster_id = cluster-from-config
14+
15+
[invalid-profile]
16+
# This profile is missing the required 'host' field
17+
cluster_id = some-cluster-id

cmd/auth/token.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ func loadToken(ctx context.Context, args loadTokenArgs) (*oauth2.Token, error) {
9292
return nil, errors.New("providing both a profile and host is not supported")
9393
}
9494

95-
err := setHostAndAccountId(ctx, args.profiler, args.profileName, args.authArguments, args.args)
95+
existingProfile, err := loadProfileByName(ctx, args.profileName, args.profiler)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
err = setHostAndAccountId(ctx, existingProfile, args.authArguments, args.args)
96101
if err != nil {
97102
return nil, err
98103
}

libs/databrickscfg/profile/file.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func (f FileProfilerImpl) LoadProfiles(ctx context.Context, fn ProfileMatchFunct
8282
Name: v.Name(),
8383
Host: host,
8484
AccountID: all["account_id"],
85+
ClusterID: all["cluster_id"],
8586
}
8687
if fn(profile) {
8788
profiles = append(profiles, profile)

libs/databrickscfg/profile/profile.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type Profile struct {
1313
Name string
1414
Host string
1515
AccountID string
16+
ClusterID string
1617
}
1718

1819
func (p Profile) Cloud() string {

0 commit comments

Comments
 (0)