Skip to content
Draft
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
26 changes: 3 additions & 23 deletions cmd/auth/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,28 @@ import (
"fmt"
"io/fs"
"net/http"
"net/url"
"strings"

"github.com/databricks/cli/libs/databrickscfg"
"github.com/databricks/cli/libs/databrickscfg/profile"
"github.com/databricks/databricks-sdk-go/config"
"github.com/spf13/cobra"
"gopkg.in/ini.v1"
)

func canonicalHost(host string) (string, error) {
parsedHost, err := url.Parse(host)
if err != nil {
return "", err
}
// If the host is empty, assume the scheme wasn't included.
if parsedHost.Host == "" {
return "https://" + host, nil
}
return "https://" + parsedHost.Host, nil
}

var ErrNoMatchingProfiles = errors.New("no matching profiles found")

func resolveSection(cfg *config.Config, iniFile *config.File) (*ini.Section, error) {
var candidates []*ini.Section
configuredHost, err := canonicalHost(cfg.Host)
if err != nil {
return nil, err
}
configuredHost := databrickscfg.NormalizeHost(cfg.Host)
for _, section := range iniFile.Sections() {
hash := section.KeysHash()
host, ok := hash["host"]
if !ok {
// if host is not set
continue
}
canonical, err := canonicalHost(host)
if err != nil {
// we're fine with other corrupt profiles
continue
}
if canonical != configuredHost {
if databrickscfg.NormalizeHost(host) != configuredHost {
continue
}
candidates = append(candidates, section)
Expand Down
2 changes: 1 addition & 1 deletion cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ depends on the existing profiles you have set in your configuration file
if profileName != "" {
err := databrickscfg.SaveToProfile(ctx, &config.Config{
Profile: profileName,
Host: authArguments.Host,
Host: databrickscfg.NormalizeHost(authArguments.Host),
AuthType: authTypeDatabricksCLI,
AccountID: authArguments.AccountID,
WorkspaceID: authArguments.WorkspaceID,
Expand Down
8 changes: 4 additions & 4 deletions cmd/configure/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ func configureInteractive(cmd *cobra.Command, flags *configureFlags, cfg *config
prompt.Label = "Databricks workspace host (https://...)"
prompt.AllowEdit = true
prompt.Validate = func(input string) error {
normalized := normalizeHost(input)
normalized := databrickscfg.NormalizeHost(input)
return validateHost(normalized)
}
out, err := prompt.Run()
if err != nil {
return err
}
cfg.Host = normalizeHost(out)
cfg.Host = databrickscfg.NormalizeHost(out)
}

// Ask user to specify the token is not already set.
Expand Down Expand Up @@ -121,15 +121,15 @@ The host must be specified with the --host flag or the DATABRICKS_HOST environme

// Populate configuration from flags (if set).
if flags.Host != "" {
cfg.Host = normalizeHost(flags.Host)
cfg.Host = databrickscfg.NormalizeHost(flags.Host)
}
if flags.Profile != "" {
cfg.Profile = flags.Profile
}

// Normalize and verify that the host is valid (if set).
if cfg.Host != "" {
cfg.Host = normalizeHost(cfg.Host)
cfg.Host = databrickscfg.NormalizeHost(cfg.Host)
err = validateHost(cfg.Host)
if err != nil {
return err
Expand Down
21 changes: 0 additions & 21 deletions cmd/configure/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,8 @@ package configure
import (
"errors"
"net/url"
"strings"
)

// normalizeHost normalizes host input to prevent double https:// prefixes.
// If the input already starts with https://, it returns it as-is.
// If the input doesn't start with https://, it prepends https://.
func normalizeHost(input string) string {
input = strings.TrimSpace(input)
u, err := url.Parse(input)
// If the input is not a valid URL, return it as-is
if err != nil {
return input
}

// If it already starts with https:// or http://, return as-is
if u.Scheme == "https" || u.Scheme == "http" {
return input
}

// Otherwise, prepend https://
return "https://" + input
}

func validateHost(s string) error {
u, err := url.Parse(s)
if err != nil {
Expand Down
35 changes: 0 additions & 35 deletions cmd/configure/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,6 @@ import (
"github.com/stretchr/testify/assert"
)

func TestNormalizeHost(t *testing.T) {
tests := []struct {
input string
expected string
}{
// Empty input
{"", "https://"},
{" ", "https://"},

// Already has https://
{"https://example.databricks.com", "https://example.databricks.com"},
{"HTTPS://EXAMPLE.DATABRICKS.COM", "HTTPS://EXAMPLE.DATABRICKS.COM"},
{"https://example.databricks.com/", "https://example.databricks.com/"},

// Missing protocol (should add https://)
{"example.databricks.com", "https://example.databricks.com"},
{" example.databricks.com ", "https://example.databricks.com"},
{"subdomain.example.databricks.com", "https://subdomain.example.databricks.com"},

// Edge cases
{"https://", "https://"},
{"example.com", "https://example.com"},
{"https://example.databricks.com/path", "https://example.databricks.com/path"},
{"https://example.databricks.com/path/", "https://example.databricks.com/path/"},
{"http://localhost:8080", "http://localhost:8080"},
}

for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
result := normalizeHost(test.input)
assert.Equal(t, test.expected, result)
})
}
}

func TestValidateHost(t *testing.T) {
var err error

Expand Down
32 changes: 23 additions & 9 deletions libs/databrickscfg/host.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
package databrickscfg

import "net/url"
import (
"net/url"
"strings"
)

// NormalizeHost returns the canonical representation of a Databricks host.
// It ensures the host has an https:// scheme and strips any path, query,
// or fragment components, returning only scheme://host[:port].
func NormalizeHost(host string) string {
host = strings.TrimSpace(host)
if host == "" {
return host
}

// If no scheme, prepend https:// before parsing.
// This is necessary because url.Parse treats schemeless input
// (e.g. "myhost.com") as a path, not a host.
if !strings.Contains(host, "://") {
host = "https://" + host
}

// normalizeHost returns the string representation of only
// the scheme and host part of the specified host.
func normalizeHost(host string) string {
u, err := url.Parse(host)
if err != nil {
return host
}
if u.Scheme == "" || u.Host == "" {
if u.Host == "" {
return host
}

normalized := &url.URL{
return (&url.URL{
Scheme: u.Scheme,
Host: u.Host,
}

return normalized.String()
}).String()
}
67 changes: 56 additions & 11 deletions libs/databrickscfg/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,65 @@ import (
)

func TestNormalizeHost(t *testing.T) {
assert.Equal(t, "invalid", normalizeHost("invalid"))
tests := []struct {
input string
expected string
}{
// Empty and whitespace.
{"", ""},
{" ", ""},

// With port.
assert.Equal(t, "http://foo:123", normalizeHost("http://foo:123"))
// Bare hostnames (no scheme).
{"foo.com", "https://foo.com"},
{"foo.com:8080", "https://foo.com:8080"},
{"e2-dogfood.staging.cloud.databricks.com", "https://e2-dogfood.staging.cloud.databricks.com"},

// With trailing slash.
assert.Equal(t, "http://foo", normalizeHost("http://foo/"))
// With https:// scheme.
{"https://foo.com", "https://foo.com"},
{"https://foo.com/", "https://foo.com"},
{"https://foo.com/path", "https://foo.com"},
{"https://foo.com?q=1", "https://foo.com"},
{"https://foo.com#frag", "https://foo.com"},
{"https://foo.com:443", "https://foo.com:443"},

// With path.
assert.Equal(t, "http://foo", normalizeHost("http://foo/bar"))
// With http:// scheme (preserved for local dev).
{"http://foo.com", "http://foo.com"},
{"http://localhost:8080", "http://localhost:8080"},
{"http://foo.com/path", "http://foo.com"},

// With query string.
assert.Equal(t, "http://foo", normalizeHost("http://foo?bar"))
// Port preserved.
{"http://foo:123", "http://foo:123"},

// With anchor.
assert.Equal(t, "http://foo", normalizeHost("http://foo#bar"))
// Whitespace trimmed.
{" https://foo.com ", "https://foo.com"},
{" foo.com ", "https://foo.com"},

// Scheme is lowercased; host case is preserved (Go's url package behavior).
{"HTTPS://FOO.COM", "https://FOO.COM"},

// Idempotent.
{"https://foo.com", "https://foo.com"},
}

for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
result := NormalizeHost(test.input)
assert.Equal(t, test.expected, result)
})
}
}

func TestNormalizeHostIdempotent(t *testing.T) {
inputs := []string{
"foo.com",
"https://foo.com/path?q=1#frag",
"http://localhost:8080",
" HTTPS://FOO.COM ",
}

for _, input := range inputs {
first := NormalizeHost(input)
second := NormalizeHost(first)
assert.Equal(t, first, second, "NormalizeHost should be idempotent for input: %s", input)
}
}
4 changes: 2 additions & 2 deletions libs/databrickscfg/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (l profileFromHostLoader) Configure(cfg *config.Config) error {
return fmt.Errorf("cannot parse config file: %w", err)
}
// Normalized version of the configured host.
host := normalizeHost(cfg.Host)
host := NormalizeHost(cfg.Host)
match, err := findMatchingProfile(configFile, func(s *ini.Section) bool {
key, err := s.GetKey("host")
if err != nil {
Expand All @@ -93,7 +93,7 @@ func (l profileFromHostLoader) Configure(cfg *config.Config) error {
}

// Check if this section matches the normalized host
return normalizeHost(key.Value()) == host
return NormalizeHost(key.Value()) == host
})
if err == errNoMatchingProfiles {
return nil
Expand Down
8 changes: 4 additions & 4 deletions libs/databrickscfg/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func matchOrCreateSection(ctx context.Context, configFile *config.File, cfg *con
return false
}
// Check if this section matches the normalized host
return normalizeHost(host) == normalizeHost(cfg.Host)
return NormalizeHost(host) == NormalizeHost(cfg.Host)
})
if err == errNoMatchingProfiles {
section, err = configFile.NewSection(cfg.Profile)
Expand Down Expand Up @@ -160,19 +160,19 @@ func ValidateConfigAndProfileHost(cfg *config.Config, profile string) error {
}

// Normalized version of the configured host.
host := normalizeHost(cfg.Host)
host := NormalizeHost(cfg.Host)
match, err := findMatchingProfile(configFile, func(s *ini.Section) bool {
return profile == s.Name()
})
if err != nil {
return err
}

hostFromProfile := normalizeHost(match.Key("host").Value())
hostFromProfile := NormalizeHost(match.Key("host").Value())
if hostFromProfile != "" && host != "" && hostFromProfile != host {
// 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
return NormalizeHost(s.Key("host").Value()) == host
})
if err == nil && match != nil {
profileName := match.Name()
Expand Down
15 changes: 5 additions & 10 deletions libs/databrickscfg/profile/profiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package profile
import (
"context"

"github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/cli/libs/databrickscfg"
)

type ProfileMatchFunction func(Profile) bool
Expand Down Expand Up @@ -47,26 +47,21 @@ func WithName(name string) ProfileMatchFunction {
// WithHost returns a ProfileMatchFunction that matches profiles whose
// canonical host equals the given host.
func WithHost(host string) ProfileMatchFunction {
target := canonicalizeHost(host)
target := databrickscfg.NormalizeHost(host)
return func(p Profile) bool {
return p.Host != "" && canonicalizeHost(p.Host) == target
return p.Host != "" && databrickscfg.NormalizeHost(p.Host) == target
}
}

// WithHostAndAccountID returns a ProfileMatchFunction that matches profiles
// by both canonical host and account ID.
func WithHostAndAccountID(host, accountID string) ProfileMatchFunction {
target := canonicalizeHost(host)
target := databrickscfg.NormalizeHost(host)
return func(p Profile) bool {
return p.Host != "" && canonicalizeHost(p.Host) == target && p.AccountID == accountID
return p.Host != "" && databrickscfg.NormalizeHost(p.Host) == target && p.AccountID == accountID
}
}

// canonicalizeHost normalizes a host using the SDK's canonical host logic.
func canonicalizeHost(host string) string {
return (&config.Config{Host: host}).CanonicalHostName()
}

type Profiler interface {
LoadProfiles(context.Context, ProfileMatchFunction) (Profiles, error)
GetPath(context.Context) (string, error)
Expand Down