Skip to content
Open
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 .nextchanges/cli/auth-profiles-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* `databricks auth profiles` now explains why a profile is not valid in the JSON output and shows `??` in the Table output when invoked with `--skip-validate` ([#5216](https://github.com/databricks/cli/pull/5216)).
9 changes: 6 additions & 3 deletions acceptance/cmd/auth/profiles/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
"host": "https://workspace.cloud.databricks.test",
"cloud": "aws",
"auth_type": "",
"valid": false
"valid": false,
"valid_reason": "validation skipped (--skip-validate)"
},
{
"name": "account-profile",
"host": "https://accounts.cloud.databricks.test",
"account_id": "test-account-123",
"cloud": "aws",
"auth_type": "",
"valid": false
"valid": false,
"valid_reason": "validation skipped (--skip-validate)"
},
{
"name": "unified-profile",
Expand All @@ -24,7 +26,8 @@
"workspace_id": "[NUMID]",
"cloud": "aws",
"auth_type": "",
"valid": false
"valid": false,
"valid_reason": "validation skipped (--skip-validate)"
}
]
}
3 changes: 3 additions & 0 deletions acceptance/cmd/auth/profiles/server-error/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions acceptance/cmd/auth/profiles/server-error/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

=== 5xx from validation endpoint: profile is invalid (NO), full error in valid_reason
>>> [CLI] auth profiles --output json
{
"profiles": [
{
"name": "transient",
"host": "[DATABRICKS_URL]",
"cloud": "aws",
"auth_type": "pat",
"valid": false,
"valid_reason": "Internal server error"
}
]
}
10 changes: 10 additions & 0 deletions acceptance/cmd/auth/profiles/server-error/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
sethome "./home"

cat > "./home/.databrickscfg" <<EOF
[transient]
host = ${DATABRICKS_HOST}
token = test-token
EOF

title "5xx from validation endpoint: profile is invalid (NO), full error in valid_reason"
trace $CLI auth profiles --output json
16 changes: 16 additions & 0 deletions acceptance/cmd/auth/profiles/server-error/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Ignore = [
"home"
]

# A 500 from the validation endpoint fails validation like any other error:
# the profile is reported valid=false with the backend error echoed verbatim
# in valid_reason, so users can see it was the platform, not their credentials.
[[Server]]
Pattern = "GET /api/2.0/preview/scim/v2/Me"
Response.StatusCode = 500
Response.Body = '''
{
"error_code": "INTERNAL_ERROR",
"message": "Internal server error"
}
'''
8 changes: 4 additions & 4 deletions acceptance/cmd/auth/switch/nominal/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ default_profile = profile-a

>>> [CLI] auth profiles --skip-validate
Name Host Valid
profile-a (Default) [DATABRICKS_URL] NO
profile-b [DATABRICKS_URL] NO
profile-a (Default) [DATABRICKS_URL] ??
profile-b [DATABRICKS_URL] ??

=== Switch to profile-b

Expand All @@ -28,5 +28,5 @@ default_profile = profile-b

>>> [CLI] auth profiles --skip-validate
Name Host Valid
profile-a [DATABRICKS_URL] NO
profile-b (Default) [DATABRICKS_URL] NO
profile-a [DATABRICKS_URL] ??
profile-b (Default) [DATABRICKS_URL] ??
90 changes: 68 additions & 22 deletions cmd/auth/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,66 @@ import (
"gopkg.in/ini.v1"
)

// profileStatus is the three-state result of listing a profile. It drives the
// YES/NO/?? cell in the text table. Only YES (validated) and NO (validation
// failed) reflect an actual check; ?? means validation was skipped.
type profileStatus string

const (
profileStatusValid profileStatus = "valid" // validation succeeded
profileStatusInvalid profileStatus = "invalid" // validation failed (any error)
profileStatusSkipped profileStatus = "skipped" // --skip-validate; not checked
)

type profileMetadata struct {
Name string `json:"name"`
Host string `json:"host,omitempty"`
AccountID string `json:"account_id,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
Cloud string `json:"cloud"`
AuthType string `json:"auth_type"`

// Valid is true only when validation conclusively succeeded. ValidReason
// explains a false result: the full underlying error when validation failed,
// or a "skipped" note under --skip-validate. It is empty only on success.
// The text table shows only YES/NO/?? (via StatusDisplay); the reason detail
// is reserved for --output json.
Valid bool `json:"valid"`
ValidReason string `json:"valid_reason,omitempty"`
Default bool `json:"default,omitempty"`

// StatusDisplay is the colored YES/NO/?? cell for the text table. Not
// serialized; the JSON form uses Valid + ValidReason instead.
StatusDisplay string `json:"-"`
}

func (c *profileMetadata) IsEmpty() bool {
return c.Host == "" && c.AccountID == ""
}

// setStatus records the validation result: Valid is true only for a
// conclusively valid profile, and reason explains a non-valid result (the
// underlying error, or a skip note) for JSON output.
func (c *profileMetadata) setStatus(ctx context.Context, status profileStatus, reason string) {
c.Valid = status == profileStatusValid
c.ValidReason = reason
c.StatusDisplay = renderStatusCell(ctx, status)
}

// renderStatusCell formats a profileStatus for the text "Valid" column:
// YES (green, validated) / NO (red, validation failed) / ?? (grey, skipped).
func renderStatusCell(ctx context.Context, s profileStatus) string {
switch s {
case profileStatusValid:
return cmdio.Green(ctx, "YES")
case profileStatusInvalid:
return cmdio.Red(ctx, "NO")
case profileStatusSkipped:
return cmdio.HiBlack(ctx, "??")
}
return cmdio.HiBlack(ctx, "??")
}

func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipValidate bool) {
cfg := &config.Config{
Loaders: []config.Loader{config.ConfigFile},
Expand Down Expand Up @@ -64,6 +109,7 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
if skipValidate {
c.Host = cfg.CanonicalHostName()
c.AuthType = cfg.AuthType
c.setStatus(ctx, profileStatusSkipped, "validation skipped (--skip-validate)")
return
}

Expand All @@ -72,35 +118,35 @@ func (c *profileMetadata) Load(ctx context.Context, configFilePath string, skipV
log.Debugf(ctx, "Profile %q: overrode config type from %s to %s (SPOG host)", c.Name, cfg.ConfigType(), configType)
}

var err error
switch configType {
case config.AccountConfig:
a, err := databricks.NewAccountClient((*databricks.Config)(cfg))
if err != nil {
return
}
_, err = a.Workspaces.List(ctx)
c.Host = cfg.Host
c.AuthType = cfg.AuthType
if err != nil {
return
var a *databricks.AccountClient
a, err = databricks.NewAccountClient((*databricks.Config)(cfg))
if err == nil {
_, err = a.Workspaces.List(ctx)
}
c.Valid = true
case config.WorkspaceConfig:
w, err := databricks.NewWorkspaceClient((*databricks.Config)(cfg))
if err != nil {
return
}
_, err = w.CurrentUser.Me(ctx, iam.MeRequest{})
c.Host = cfg.Host
c.AuthType = cfg.AuthType
if err != nil {
return
var w *databricks.WorkspaceClient
w, err = databricks.NewWorkspaceClient((*databricks.Config)(cfg))
if err == nil {
_, err = w.CurrentUser.Me(ctx, iam.MeRequest{})
}
c.Valid = true
case config.InvalidConfig:
// Invalid configuration, skip validation
c.setStatus(ctx, profileStatusInvalid, "profile fields conflict (e.g. workspace and account configured together)")
return
}

c.Host = cfg.Host
c.AuthType = cfg.AuthType

// Any validation error means NO; the full error is preserved in ValidReason
// for --output json. The text table shows only the YES/NO/?? cell.
if err != nil {
c.setStatus(ctx, profileStatusInvalid, err.Error())
return
}
c.setStatus(ctx, profileStatusValid, "")
}

func newProfilesCommand() *cobra.Command {
Expand All @@ -110,7 +156,7 @@ func newProfilesCommand() *cobra.Command {
Annotations: map[string]string{
"template": cmdio.Heredoc(`
{{header "Name"}} {{header "Host"}} {{header "Valid"}}
{{range .Profiles}}{{.Name | green}}{{if .Default}} (Default){{end}} {{.Host|cyan}} {{bool .Valid}}
{{range .Profiles}}{{.Name | green}}{{if .Default}} (Default){{end}} {{.Host|cyan}} {{.StatusDisplay}}
{{end}}`),
},
}
Expand Down
Loading
Loading