From b35960653a32ae90c2dc30be9827b7de604df35f Mon Sep 17 00:00:00 2001 From: Rada Kamysheva Date: Fri, 19 Jun 2026 11:48:42 +0000 Subject: [PATCH] Improve error message on auth host mismatch When workspace.host does not match the host of the selected profile, render a located, multi-line diagnostic (file:line:col) that names the profile and both hosts and suggests a matching profile when one exists, instead of an opaque "cannot resolve bundle auth configuration" error. Detection is centralized in a typed databrickscfg.HostMismatchError, and the rich diagnostic is rendered at the configureBundle choke point so it applies to all bundle commands. --- NEXT_CHANGELOG.md | 1 + acceptance/auth/bundle_and_profile/output.txt | 27 +++++++++-- cmd/root/bundle.go | 48 +++++++++++++++++++ cmd/root/bundle_test.go | 17 ++++++- libs/databrickscfg/ops.go | 37 ++++++++++++-- libs/databrickscfg/ops_test.go | 33 +++++++++++++ 6 files changed, 155 insertions(+), 8 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 1cec48c3f50..b77ace4f064 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### CLI ### Bundles +* Improve the error shown when `workspace.host` does not match the host of the selected profile: it now points at the offending location in your configuration files and suggests a matching profile when one exists. * `bundle run` now prints the modern job run URL (`/jobs//runs/`) so that non-admin users permitted to view the run are taken to the run instead of the workspace homepage. * Fix missing field descriptions in the bundle JSON schema for fields whose upstream API docs arrived after the field was first annotated (e.g. `vector_search_endpoints.*.target_qps`); stale placeholder markers no longer hide them ([#5588](https://github.com/databricks/cli/pull/5588)). diff --git a/acceptance/auth/bundle_and_profile/output.txt b/acceptance/auth/bundle_and_profile/output.txt index 98347d64007..6e56cbca474 100644 --- a/acceptance/auth/bundle_and_profile/output.txt +++ b/acceptance/auth/bundle_and_profile/output.txt @@ -20,7 +20,14 @@ Exit code: 1 === Inside the bundle, target and not matching profile >>> errcode [CLI] current-user me -t dev -p profile_name -Error: cannot resolve bundle auth configuration: the host in the profile (https://non.existing.subdomain.databricks.test) doesn’t match the host configured in the bundle ([DATABRICKS_TARGET]). The profile "DEFAULT" has host="[DATABRICKS_TARGET]" that matches host in the bundle. To select it, pass "-p DEFAULT" +Error: workspace host does not match the selected profile + at workspace.host + in databricks.yml:11:13 + databricks.yml:5:9 + +The host configured in workspace.host ([DATABRICKS_TARGET]) does not match the host of profile "profile_name" (https://non.existing.subdomain.databricks.test) used for authentication. + +Profile "DEFAULT" matches the bundle host. To use it, set workspace.profile to "DEFAULT" or pass -p DEFAULT on the command line. Exit code: 1 @@ -49,7 +56,14 @@ Validation OK! === Bundle commands load bundle configuration with -p flag, validation not OK (profile host don't match bundle host) >>> errcode [CLI] bundle validate -p profile_name -Error: cannot resolve bundle auth configuration: the host in the profile (https://non.existing.subdomain.databricks.test) doesn’t match the host configured in the bundle ([DATABRICKS_TARGET]). The profile "DEFAULT" has host="[DATABRICKS_TARGET]" that matches host in the bundle. To select it, pass "-p DEFAULT" +Error: workspace host does not match the selected profile + at workspace.host + in databricks.yml:11:13 + databricks.yml:5:9 + +The host configured in workspace.host ([DATABRICKS_TARGET]) does not match the host of profile "profile_name" (https://non.existing.subdomain.databricks.test) used for authentication. + +Profile "DEFAULT" matches the bundle host. To use it, set workspace.profile to "DEFAULT" or pass -p DEFAULT on the command line. Name: test-auth Target: dev @@ -74,7 +88,14 @@ Validation OK! === Bundle commands load bundle configuration with -t and -p flag, validation not OK (profile host don't match bundle host) >>> errcode [CLI] bundle validate -t prod -p DEFAULT Warn: [hostmetadata] failed to fetch host metadata for https://bar.test, will skip for 1m0s -Error: cannot resolve bundle auth configuration: the host in the profile ([DATABRICKS_TARGET]) doesn’t match the host configured in the bundle (https://bar.test) +Error: workspace host does not match the selected profile + at workspace.host + in databricks.yml:14:13 + databricks.yml:5:9 + +The host configured in workspace.host (https://bar.test) does not match the host of profile "DEFAULT" ([DATABRICKS_TARGET]) used for authentication. + +Update workspace.host or workspace.profile so they refer to the same workspace. Name: test-auth Target: prod diff --git a/cmd/root/bundle.go b/cmd/root/bundle.go index a17d88f5fcf..04426ec0d35 100644 --- a/cmd/root/bundle.go +++ b/cmd/root/bundle.go @@ -15,6 +15,8 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/databrickscfg" "github.com/databricks/cli/libs/databrickscfg/profile" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" envlib "github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" @@ -152,6 +154,44 @@ func resolveProfileAmbiguity(cmd *cobra.Command, b *bundle.Bundle, originalErr e }) } +// hostMismatchDiagnostic builds a located, multi-line diagnostic for a +// host/profile mismatch. It points at workspace.host (and workspace.profile +// when it is set in the configuration files) so the user can see where each +// value comes from. +func hostMismatchDiagnostic(b *bundle.Bundle, mismatch *databrickscfg.HostMismatchError) diag.Diagnostic { + detail := fmt.Sprintf( + "The host configured in workspace.host (%s) does not match the host of profile %q (%s) used for authentication.", + mismatch.ConfiguredHost, mismatch.Profile, mismatch.ProfileHost, + ) + if mismatch.SuggestedProfile != "" { + detail += fmt.Sprintf( + "\n\nProfile %q matches the bundle host. To use it, set workspace.profile to %q or pass -p %s on the command line.", + mismatch.SuggestedProfile, mismatch.SuggestedProfile, mismatch.SuggestedProfile, + ) + } else { + detail += "\n\nUpdate workspace.host or workspace.profile so they refer to the same workspace." + } + + paths := []dyn.Path{dyn.MustPathFromString("workspace.host")} + locations := b.Config.GetLocations("workspace.host") + + // workspace.profile may be set via a flag or environment variable, in which + // case it has no location in the configuration files. Only point at it when + // it is actually defined there. + if profileLocations := b.Config.GetLocations("workspace.profile"); len(profileLocations) > 0 { + paths = append(paths, dyn.MustPathFromString("workspace.profile")) + locations = append(locations, profileLocations...) + } + + return diag.Diagnostic{ + Severity: diag.Error, + Summary: "workspace host does not match the selected profile", + Detail: detail, + Locations: locations, + Paths: paths, + } +} + // configureBundle loads the bundle configuration and configures flag values, if any. func configureBundle(cmd *cobra.Command, b *bundle.Bundle) { // Load bundle and select target. @@ -176,6 +216,14 @@ func configureBundle(cmd *cobra.Command, b *bundle.Bundle) { // is a fast operation. It does not perform network I/O or invoke processes (for example the Azure CLI). client, err := b.WorkspaceClientE(ctx) if err != nil { + // A host/profile mismatch is a configuration error we can point at + // directly in the bundle files, so render it as a located diagnostic + // instead of an opaque auth-resolution failure. + if mismatch, ok := errors.AsType[*databrickscfg.HostMismatchError](err); ok { + logdiag.LogDiag(ctx, hostMismatchDiagnostic(b, mismatch)) + return + } + names, isMulti := databrickscfg.AsMultipleProfiles(err) if !isMulti { logdiag.LogError(ctx, err) diff --git a/cmd/root/bundle_test.go b/cmd/root/bundle_test.go index 6116003ea70..b9b62c2069c 100644 --- a/cmd/root/bundle_test.go +++ b/cmd/root/bundle_test.go @@ -13,6 +13,7 @@ import ( "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/logdiag" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" @@ -152,7 +153,13 @@ func TestBundleConfigureWithMismatchedProfile(t *testing.T) { require.NoError(t, err) diags := setupWithHost(t, cmd, "https://x.test") - assert.Equal(t, []diag.Diagnostic{{Summary: "cannot resolve bundle auth configuration: the host in the profile (https://a.test) doesn’t match the host configured in the bundle (https://x.test)"}}, diags) + assert.Equal(t, []diag.Diagnostic{{ + Severity: diag.Error, + Summary: "workspace host does not match the selected profile", + Detail: "The host configured in workspace.host (https://x.test) does not match the host of profile \"PROFILE-1\" (https://a.test) used for authentication.\n\nUpdate workspace.host or workspace.profile so they refer to the same workspace.", + Locations: []dyn.Location{{File: "databricks.yml", Line: 3, Column: 9}}, + Paths: []dyn.Path{dyn.MustPathFromString("workspace.host")}, + }}, diags) } func TestBundleConfigureWithCorrectProfile(t *testing.T) { @@ -175,7 +182,13 @@ func TestBundleConfigureWithMismatchedProfileEnvVariable(t *testing.T) { cmd := emptyCommand(t) diags := setupWithHost(t, cmd, "https://x.test") - assert.Equal(t, []diag.Diagnostic{{Summary: "cannot resolve bundle auth configuration: the host in the profile (https://a.test) doesn’t match the host configured in the bundle (https://x.test)"}}, diags) + assert.Equal(t, []diag.Diagnostic{{ + Severity: diag.Error, + Summary: "workspace host does not match the selected profile", + Detail: "The host configured in workspace.host (https://x.test) does not match the host of profile \"PROFILE-1\" (https://a.test) used for authentication.\n\nUpdate workspace.host or workspace.profile so they refer to the same workspace.", + Locations: []dyn.Location{{File: "databricks.yml", Line: 3, Column: 9}}, + Paths: []dyn.Path{dyn.MustPathFromString("workspace.host")}, + }}, diags) } func TestBundleConfigureWithProfileFlagAndEnvVariable(t *testing.T) { diff --git a/libs/databrickscfg/ops.go b/libs/databrickscfg/ops.go index 2eb58c2cc78..0d4202d623f 100644 --- a/libs/databrickscfg/ops.go +++ b/libs/databrickscfg/ops.go @@ -478,6 +478,32 @@ func DeleteProfile(ctx context.Context, profileName, configFilePath string) erro return writeConfigFile(ctx, configFile) } +// HostMismatchError reports that the host configured in the bundle does not +// match the host of the profile selected for authentication. It carries the +// normalized hosts and profile names so callers can render richer messages +// (e.g. a diagnostic with source locations) without re-deriving them. +type HostMismatchError struct { + // ConfiguredHost is the normalized host configured in the bundle. + ConfiguredHost string + + // ProfileHost is the normalized host of the selected Profile. + ProfileHost string + + // Profile is the name of the selected profile. + Profile string + + // SuggestedProfile is the name of a profile whose host matches + // ConfiguredHost, or "" if no such profile exists. + SuggestedProfile string +} + +func (e *HostMismatchError) Error() string { + if e.SuggestedProfile != "" { + return fmt.Sprintf("the host in the profile (%s) doesn’t match the host configured in the bundle (%s). The profile \"%s\" has host=\"%s\" that matches host in the bundle. To select it, pass \"-p %s\"", e.ProfileHost, e.ConfiguredHost, e.SuggestedProfile, e.ConfiguredHost, e.SuggestedProfile) + } + return fmt.Sprintf("the host in the profile (%s) doesn’t match the host configured in the bundle (%s)", e.ProfileHost, e.ConfiguredHost) +} + func ValidateConfigAndProfileHost(cfg *config.Config, profile string) error { configFile, err := config.LoadFile(cfg.ConfigFile) if err != nil { @@ -495,16 +521,21 @@ func ValidateConfigAndProfileHost(cfg *config.Config, profile string) error { hostFromProfile := normalizeHost(match.Key("host").Value()) if hostFromProfile != "" && host != "" && hostFromProfile != host { + mismatch := &HostMismatchError{ + ConfiguredHost: host, + ProfileHost: hostFromProfile, + Profile: profile, + } + // Try to find if there's a profile which uses the same host as the bundle and suggest in error message match, err = findMatchingProfile(configFile, func(s *ini.Section) bool { return normalizeHost(s.Key("host").Value()) == host }) if err == nil && match != nil { - profileName := match.Name() - return fmt.Errorf("the host in the profile (%s) doesn’t match the host configured in the bundle (%s). The profile \"%s\" has host=\"%s\" that matches host in the bundle. To select it, pass \"-p %s\"", hostFromProfile, host, profileName, host, profileName) + mismatch.SuggestedProfile = match.Name() } - return fmt.Errorf("the host in the profile (%s) doesn’t match the host configured in the bundle (%s)", hostFromProfile, host) + return mismatch } return nil diff --git a/libs/databrickscfg/ops_test.go b/libs/databrickscfg/ops_test.go index 480054bfed6..05a2076dd29 100644 --- a/libs/databrickscfg/ops_test.go +++ b/libs/databrickscfg/ops_test.go @@ -37,6 +37,39 @@ func TestLoadOrCreate_Bad(t *testing.T) { assert.Nil(t, file) } +func TestValidateConfigAndProfileHost(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "databrickscfg") + contents := "[abc]\nhost = https://abc.test\ntoken = a\n\n[def]\nhost = https://def.test\ntoken = d\n" + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + + t.Run("match", func(t *testing.T) { + cfg := &config.Config{Host: "https://abc.test", ConfigFile: path} + assert.NoError(t, ValidateConfigAndProfileHost(cfg, "abc")) + }) + + t.Run("mismatch with suggested profile", func(t *testing.T) { + cfg := &config.Config{Host: "https://abc.test", ConfigFile: path} + err := ValidateConfigAndProfileHost(cfg, "def") + + var mismatch *HostMismatchError + require.ErrorAs(t, err, &mismatch) + assert.Equal(t, "https://abc.test", mismatch.ConfiguredHost) + assert.Equal(t, "https://def.test", mismatch.ProfileHost) + assert.Equal(t, "def", mismatch.Profile) + assert.Equal(t, "abc", mismatch.SuggestedProfile) + }) + + t.Run("mismatch without suggested profile", func(t *testing.T) { + cfg := &config.Config{Host: "https://ghi.test", ConfigFile: path} + err := ValidateConfigAndProfileHost(cfg, "def") + + var mismatch *HostMismatchError + require.ErrorAs(t, err, &mismatch) + assert.Empty(t, mismatch.SuggestedProfile) + }) +} + func TestMatchOrCreateSection_Direct(t *testing.T) { cfg := &config.Config{ Profile: "query",