Skip to content
Closed
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 @@ -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/<id>/runs/<id>`) 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)).

Expand Down
27 changes: 24 additions & 3 deletions acceptance/auth/bundle_and_profile/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
48 changes: 48 additions & 0 deletions cmd/root/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions cmd/root/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
37 changes: 34 additions & 3 deletions libs/databrickscfg/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions libs/databrickscfg/ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading