Skip to content

Commit 4b394b8

Browse files
committed
Fix doctor command: account-level auth, per-check timeouts, network fallback, skip status
- Detect account-level configs (AccountID + account host) and use NewAccountClient instead of always using NewWorkspaceClient - Add 15s per-check deadline for auth and identity checks to prevent hangs on unresponsive IdP - Network check now tries even when config resolution fails, as long as a host URL is available from partial config resolution - Identity marked as 'skip' (not 'fail') when auth failed or when using account-level profile, avoiding double failures from one root cause - Add skip status rendering in text output
1 parent 606a69b commit 4b394b8

3 files changed

Lines changed: 179 additions & 58 deletions

File tree

cmd/doctor/checks.go

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ const (
2424
statusFail = "fail"
2525
statusWarn = "warn"
2626
statusInfo = "info"
27+
statusSkip = "skip"
2728

2829
networkTimeout = 10 * time.Second
30+
checkTimeout = 15 * time.Second
2931
)
3032

3133
// runChecks runs all diagnostic checks and returns the results.
@@ -38,20 +40,20 @@ func runChecks(cmd *cobra.Command) []CheckResult {
3840
results = append(results, checkConfigFile(cmd))
3941
results = append(results, checkCurrentProfile(cmd))
4042

41-
authResult, w := checkAuth(cmd, cfg, err)
43+
authResult, authCfg := checkAuth(cmd, cfg, err)
4244
results = append(results, authResult)
4345

44-
if w != nil {
45-
results = append(results, checkIdentity(cmd, w))
46+
if authCfg != nil {
47+
results = append(results, checkIdentity(cmd, authCfg))
4648
} else {
4749
results = append(results, CheckResult{
4850
Name: "Identity",
49-
Status: statusFail,
51+
Status: statusSkip,
5052
Message: "Skipped (authentication failed)",
5153
})
5254
}
5355

54-
results = append(results, checkNetwork(cmd, cfg, err, w))
56+
results = append(results, checkNetwork(cmd, cfg, err, authCfg))
5557
return results
5658
}
5759

@@ -154,9 +156,17 @@ func resolveConfig(cmd *cobra.Command) (*config.Config, error) {
154156
return cfg, cfg.EnsureResolved()
155157
}
156158

159+
// isAccountLevelConfig returns true if the resolved config targets account-level APIs.
160+
func isAccountLevelConfig(cfg *config.Config) bool {
161+
return cfg.AccountID != "" && cfg.Host != "" && cfg.HostType() == config.AccountHost
162+
}
163+
157164
// checkAuth uses the resolved config to authenticate.
158-
func checkAuth(cmd *cobra.Command, cfg *config.Config, resolveErr error) (CheckResult, *databricks.WorkspaceClient) {
159-
ctx := cmd.Context()
165+
// On success it returns the authenticated config for use in subsequent checks.
166+
func checkAuth(cmd *cobra.Command, cfg *config.Config, resolveErr error) (CheckResult, *config.Config) {
167+
ctx, cancel := context.WithTimeout(cmd.Context(), checkTimeout)
168+
defer cancel()
169+
160170
if resolveErr != nil {
161171
return CheckResult{
162172
Name: "Authentication",
@@ -166,14 +176,31 @@ func checkAuth(cmd *cobra.Command, cfg *config.Config, resolveErr error) (CheckR
166176
}, nil
167177
}
168178

169-
w, err := databricks.NewWorkspaceClient((*databricks.Config)(cfg))
170-
if err != nil {
171-
return CheckResult{
172-
Name: "Authentication",
173-
Status: statusFail,
174-
Message: "Cannot create workspace client",
175-
Detail: err.Error(),
176-
}, nil
179+
// Detect account-level configs and use the appropriate client constructor
180+
// so that account profiles are not incorrectly reported as broken.
181+
var authCfg *config.Config
182+
if isAccountLevelConfig(cfg) {
183+
a, err := databricks.NewAccountClient((*databricks.Config)(cfg))
184+
if err != nil {
185+
return CheckResult{
186+
Name: "Authentication",
187+
Status: statusFail,
188+
Message: "Cannot create account client",
189+
Detail: err.Error(),
190+
}, nil
191+
}
192+
authCfg = a.Config
193+
} else {
194+
w, err := databricks.NewWorkspaceClient((*databricks.Config)(cfg))
195+
if err != nil {
196+
return CheckResult{
197+
Name: "Authentication",
198+
Status: statusFail,
199+
Message: "Cannot create workspace client",
200+
Detail: err.Error(),
201+
}, nil
202+
}
203+
authCfg = w.Config
177204
}
178205

179206
req, err := http.NewRequestWithContext(ctx, "", "", nil)
@@ -186,7 +213,7 @@ func checkAuth(cmd *cobra.Command, cfg *config.Config, resolveErr error) (CheckR
186213
}, nil
187214
}
188215

189-
err = w.Config.Authenticate(req)
216+
err = authCfg.Authenticate(req)
190217
if err != nil {
191218
return CheckResult{
192219
Name: "Authentication",
@@ -196,15 +223,41 @@ func checkAuth(cmd *cobra.Command, cfg *config.Config, resolveErr error) (CheckR
196223
}, nil
197224
}
198225

226+
msg := fmt.Sprintf("OK (%s)", authCfg.AuthType)
227+
if isAccountLevelConfig(cfg) {
228+
msg += " [account-level]"
229+
}
230+
199231
return CheckResult{
200232
Name: "Authentication",
201233
Status: statusPass,
202-
Message: fmt.Sprintf("OK (%s)", w.Config.AuthType),
203-
}, w
234+
Message: msg,
235+
}, authCfg
204236
}
205237

206-
func checkIdentity(cmd *cobra.Command, w *databricks.WorkspaceClient) CheckResult {
207-
ctx := cmd.Context()
238+
func checkIdentity(cmd *cobra.Command, authCfg *config.Config) CheckResult {
239+
ctx, cancel := context.WithTimeout(cmd.Context(), checkTimeout)
240+
defer cancel()
241+
242+
// Account-level configs don't support the /me endpoint for workspace identity.
243+
if authCfg.HostType() == config.AccountHost {
244+
return CheckResult{
245+
Name: "Identity",
246+
Status: statusSkip,
247+
Message: "Skipped (account-level profile, workspace identity not available)",
248+
}
249+
}
250+
251+
w, err := databricks.NewWorkspaceClient((*databricks.Config)(authCfg))
252+
if err != nil {
253+
return CheckResult{
254+
Name: "Identity",
255+
Status: statusFail,
256+
Message: "Cannot create workspace client",
257+
Detail: err.Error(),
258+
}
259+
}
260+
208261
me, err := w.CurrentUser.Me(ctx)
209262
if err != nil {
210263
return CheckResult{
@@ -222,24 +275,30 @@ func checkIdentity(cmd *cobra.Command, w *databricks.WorkspaceClient) CheckResul
222275
}
223276
}
224277

225-
func checkNetwork(cmd *cobra.Command, cfg *config.Config, resolveErr error, w *databricks.WorkspaceClient) CheckResult {
226-
if resolveErr != nil {
227-
return CheckResult{
228-
Name: "Network",
229-
Status: statusFail,
230-
Message: "Cannot resolve config",
231-
Detail: resolveErr.Error(),
232-
}
278+
func checkNetwork(cmd *cobra.Command, cfg *config.Config, resolveErr error, authCfg *config.Config) CheckResult {
279+
// Prefer the authenticated config (it has the fully resolved host).
280+
if authCfg != nil {
281+
return checkNetworkWithHost(cmd, authCfg.Host, configuredNetworkHTTPClient(authCfg))
233282
}
234283

235-
if w != nil {
236-
return checkNetworkWithHost(cmd, w.Config.Host, configuredNetworkHTTPClient(w.Config))
284+
// Auth failed or was skipped. If we still have a host from config resolution
285+
// (even if resolution had other errors), attempt the network check.
286+
if cfg != nil && cfg.Host != "" {
287+
log.Warnf(cmd.Context(), "authenticated client unavailable for network check, using config-based HTTP client")
288+
return checkNetworkWithHost(cmd, cfg.Host, configuredNetworkHTTPClient(cfg))
237289
}
238290

239-
// Workspace client unavailable, but we can still build an HTTP client
240-
// from the resolved config to respect proxy and TLS settings.
241-
log.Warnf(cmd.Context(), "workspace client unavailable for network check, using config-based HTTP client")
242-
return checkNetworkWithHost(cmd, cfg.Host, configuredNetworkHTTPClient(cfg))
291+
// No host available at all.
292+
detail := "no host configured"
293+
if resolveErr != nil {
294+
detail = resolveErr.Error()
295+
}
296+
return CheckResult{
297+
Name: "Network",
298+
Status: statusFail,
299+
Message: "No host configured",
300+
Detail: detail,
301+
}
243302
}
244303

245304
func checkNetworkWithHost(cmd *cobra.Command, host string, client *http.Client) CheckResult {

cmd/doctor/doctor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func renderResults(w io.Writer, results []CheckResult) {
7878
icon = yellow("[warn]")
7979
case statusInfo:
8080
icon = cyan("[info]")
81+
case statusSkip:
82+
icon = yellow("[skip]")
8183
}
8284
msg := fmt.Sprintf("%s %s: %s", icon, bold(r.Name), r.Message)
8385
if r.Detail != "" {

0 commit comments

Comments
 (0)