mirror of https://github.com/databricks/cli.git
Added `auth describe` command (#1244)
## Changes This command provide details on auth configuration user is using as well as authenticated user and auth mechanism used. Relies on https://github.com/databricks/databricks-sdk-go/pull/838 (tests will fail until merged) Examples of output ``` Workspace: https://test.com User: andrew.nester@databricks.com Authenticated with: pat ----- Configuration: ✓ auth_type: pat ✓ host: https://test.com (from bundle) ✓ profile: DEFAULT (from --profile flag) ✓ token: ******** (from /Users/andrew.nester/.databrickscfg config file) ``` ``` DATABRICKS_AUTH_TYPE=azure-msi databricks auth describe -p "Azure 2" Unable to authenticate: inner token: Post "https://foobar.com/oauth2/token": AADSTS900023: Specified tenant identifier foobar_aaaaaaa' is neither a valid DNS name, nor a valid external domain. See https://login.microsoftonline.com/error?code=900023 ----- Configuration: ✓ auth_type: azure-msi (from DATABRICKS_AUTH_TYPE environment variable) ✓ azure_client_id: 8470f3ba-aaaa-bbbb-cccc-xxxxyyyyzzzz (from /Users/andrew.nester/.databrickscfg config file) ~ azure_client_secret: ******** (from /Users/andrew.nester/.databrickscfg config file, not used for auth type azure-msi) ~ azure_tenant_id: foobar_aaaaaaa (from /Users/andrew.nester/.databrickscfg config file, not used for auth type azure-msi) ✓ azure_use_msi: true (from /Users/andrew.nester/.databrickscfg config file) ✓ host: https://foobar.com (from /Users/andrew.nester/.databrickscfg config file) ✓ profile: Azure 2 (from --profile flag) ``` For account ``` Unable to authenticate: default auth: databricks-cli: cannot get access token: Error: token refresh: Post "https://xxxxxxx.com/v1/token": http 400: {"error":"invalid_request","error_description":"Refresh token is invalid"} . Config: host=https://xxxxxxx.com, account_id=ed0ca3c5-fae5-4619-bb38-eebe04a4af4b, profile=ACCOUNT-ed0ca3c5-fae5-4619-bb38-eebe04a4af4b ----- Configuration: ✓ account_id: ed0ca3c5-fae5-4619-bb38-eebe04a4af4b (from /Users/andrew.nester/.databrickscfg config file) ✓ auth_type: databricks-cli (from /Users/andrew.nester/.databrickscfg config file) ✓ host: https://xxxxxxxxx.com (from /Users/andrew.nester/.databrickscfg config file) ✓ profile: ACCOUNT-ed0ca3c5-fae5-4619-bb38-eebe04a4af4b ``` ## Tests Added unit tests --------- Co-authored-by: Julia Crawford (Databricks) <julia.crawford@databricks.com>
This commit is contained in:
parent
079c416f8d
commit
8c144a2de4
|
@ -78,8 +78,8 @@ func (s User) MarshalJSON() ([]byte, error) {
|
||||||
return marshal.Marshal(s)
|
return marshal.Marshal(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Workspace) Client() (*databricks.WorkspaceClient, error) {
|
func (w *Workspace) Config() *config.Config {
|
||||||
cfg := config.Config{
|
cfg := &config.Config{
|
||||||
// Generic
|
// Generic
|
||||||
Host: w.Host,
|
Host: w.Host,
|
||||||
Profile: w.Profile,
|
Profile: w.Profile,
|
||||||
|
@ -101,6 +101,19 @@ func (w *Workspace) Client() (*databricks.WorkspaceClient, error) {
|
||||||
AzureLoginAppID: w.AzureLoginAppID,
|
AzureLoginAppID: w.AzureLoginAppID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k := range config.ConfigAttributes {
|
||||||
|
attr := &config.ConfigAttributes[k]
|
||||||
|
if !attr.IsZero(cfg) {
|
||||||
|
cfg.SetAttrSource(attr, config.Source{Type: config.SourceType("bundle")})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Workspace) Client() (*databricks.WorkspaceClient, error) {
|
||||||
|
cfg := w.Config()
|
||||||
|
|
||||||
// If only the host is configured, we try and unambiguously match it to
|
// If only the host is configured, we try and unambiguously match it to
|
||||||
// a profile in the user's databrickscfg file. Override the default loaders.
|
// a profile in the user's databrickscfg file. Override the default loaders.
|
||||||
if w.Host != "" && w.Profile == "" {
|
if w.Host != "" && w.Profile == "" {
|
||||||
|
@ -124,13 +137,13 @@ func (w *Workspace) Client() (*databricks.WorkspaceClient, error) {
|
||||||
// Now that the configuration is resolved, we can verify that the host in the bundle configuration
|
// Now that the configuration is resolved, we can verify that the host in the bundle configuration
|
||||||
// is identical to the host associated with the selected profile.
|
// is identical to the host associated with the selected profile.
|
||||||
if w.Host != "" && w.Profile != "" {
|
if w.Host != "" && w.Profile != "" {
|
||||||
err := databrickscfg.ValidateConfigAndProfileHost(&cfg, w.Profile)
|
err := databrickscfg.ValidateConfigAndProfileHost(cfg, w.Profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return databricks.NewWorkspaceClient((*databricks.Config)(&cfg))
|
return databricks.NewWorkspaceClient((*databricks.Config)(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -22,6 +22,7 @@ func New() *cobra.Command {
|
||||||
cmd.AddCommand(newLoginCommand(&perisistentAuth))
|
cmd.AddCommand(newLoginCommand(&perisistentAuth))
|
||||||
cmd.AddCommand(newProfilesCommand())
|
cmd.AddCommand(newProfilesCommand())
|
||||||
cmd.AddCommand(newTokenCommand(&perisistentAuth))
|
cmd.AddCommand(newTokenCommand(&perisistentAuth))
|
||||||
|
cmd.AddCommand(newDescribeCommand())
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/cmd/root"
|
||||||
|
"github.com/databricks/cli/libs/cmdio"
|
||||||
|
"github.com/databricks/cli/libs/flags"
|
||||||
|
"github.com/databricks/databricks-sdk-go/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authTemplate = `{{"Host:" | bold}} {{.Details.Host}}
|
||||||
|
{{- if .AccountID}}
|
||||||
|
{{"Account ID:" | bold}} {{.AccountID}}
|
||||||
|
{{- end}}
|
||||||
|
{{- if .Username}}
|
||||||
|
{{"User:" | bold}} {{.Username}}
|
||||||
|
{{- end}}
|
||||||
|
{{"Authenticated with:" | bold}} {{.Details.AuthType}}
|
||||||
|
-----
|
||||||
|
` + configurationTemplate
|
||||||
|
|
||||||
|
var errorTemplate = `Unable to authenticate: {{.Error}}
|
||||||
|
-----
|
||||||
|
` + configurationTemplate
|
||||||
|
|
||||||
|
const configurationTemplate = `Current configuration:
|
||||||
|
{{- $details := .Status.Details}}
|
||||||
|
{{- range $a := .ConfigAttributes}}
|
||||||
|
{{- $k := $a.Name}}
|
||||||
|
{{- if index $details.Configuration $k}}
|
||||||
|
{{- $v := index $details.Configuration $k}}
|
||||||
|
{{if $v.AuthTypeMismatch}}~{{else}}✓{{end}} {{$k | bold}}: {{$v.Value}}
|
||||||
|
{{- if not (eq $v.Source.String "dynamic configuration")}}
|
||||||
|
{{- " (from" | italic}} {{$v.Source.String | italic}}
|
||||||
|
{{- if $v.AuthTypeMismatch}}, {{ "not used for auth type " | red | italic }}{{$details.AuthType | red | italic}}{{end}})
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
func newDescribeCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "describe",
|
||||||
|
Short: "Describes the credentials and the source of those credentials, being used by the CLI to authenticate",
|
||||||
|
}
|
||||||
|
|
||||||
|
var showSensitive bool
|
||||||
|
cmd.Flags().BoolVar(&showSensitive, "sensitive", false, "Include sensitive fields like passwords and tokens in the output")
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
var status *authStatus
|
||||||
|
var err error
|
||||||
|
status, err = getAuthStatus(cmd, args, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
|
||||||
|
isAccount, err := root.MustAnyClient(cmd, args)
|
||||||
|
return root.ConfigUsed(cmd.Context()), isAccount, err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Error != nil {
|
||||||
|
return render(ctx, cmd, status, errorTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(ctx, cmd, status, authTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type tryAuth func(cmd *cobra.Command, args []string) (*config.Config, bool, error)
|
||||||
|
|
||||||
|
func getAuthStatus(cmd *cobra.Command, args []string, showSensitive bool, fn tryAuth) (*authStatus, error) {
|
||||||
|
cfg, isAccount, err := fn(cmd, args)
|
||||||
|
ctx := cmd.Context()
|
||||||
|
if err != nil {
|
||||||
|
return &authStatus{
|
||||||
|
Status: "error",
|
||||||
|
Error: err,
|
||||||
|
Details: getAuthDetails(cmd, cfg, showSensitive),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAccount {
|
||||||
|
a := root.AccountClient(ctx)
|
||||||
|
|
||||||
|
// Doing a simple API call to check if the auth is valid
|
||||||
|
_, err := a.Workspaces.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return &authStatus{
|
||||||
|
Status: "error",
|
||||||
|
Error: err,
|
||||||
|
Details: getAuthDetails(cmd, cfg, showSensitive),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status := authStatus{
|
||||||
|
Status: "success",
|
||||||
|
Details: getAuthDetails(cmd, a.Config, showSensitive),
|
||||||
|
AccountID: a.Config.AccountID,
|
||||||
|
Username: a.Config.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w := root.WorkspaceClient(ctx)
|
||||||
|
me, err := w.CurrentUser.Me(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return &authStatus{
|
||||||
|
Status: "error",
|
||||||
|
Error: err,
|
||||||
|
Details: getAuthDetails(cmd, cfg, showSensitive),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status := authStatus{
|
||||||
|
Status: "success",
|
||||||
|
Details: getAuthDetails(cmd, w.Config, showSensitive),
|
||||||
|
Username: me.UserName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(ctx context.Context, cmd *cobra.Command, status *authStatus, template string) error {
|
||||||
|
switch root.OutputType(cmd) {
|
||||||
|
case flags.OutputText:
|
||||||
|
return cmdio.RenderWithTemplate(ctx, map[string]any{
|
||||||
|
"Status": status,
|
||||||
|
"ConfigAttributes": config.ConfigAttributes,
|
||||||
|
}, "", template)
|
||||||
|
case flags.OutputJSON:
|
||||||
|
buf, err := json.MarshalIndent(status, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.OutOrStdout().Write(buf)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown output type %s", root.OutputType(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type authStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error error `json:"error,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
AccountID string `json:"account_id,omitempty"`
|
||||||
|
Details config.AuthDetails `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthDetails(cmd *cobra.Command, cfg *config.Config, showSensitive bool) config.AuthDetails {
|
||||||
|
var opts []config.AuthDetailsOptions
|
||||||
|
if showSensitive {
|
||||||
|
opts = append(opts, config.ShowSensitive)
|
||||||
|
}
|
||||||
|
details := cfg.GetAuthDetails(opts...)
|
||||||
|
|
||||||
|
for k, v := range details.Configuration {
|
||||||
|
if k == "profile" && cmd.Flag("profile").Changed {
|
||||||
|
v.Source = config.Source{Type: config.SourceType("flag"), Name: "--profile"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == "host" && cmd.Flag("host").Changed {
|
||||||
|
v.Source = config.Source{Type: config.SourceType("flag"), Name: "--host"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If profile is not set explicitly, default to "default"
|
||||||
|
if _, ok := details.Configuration["profile"]; !ok {
|
||||||
|
profile := cfg.Profile
|
||||||
|
if profile == "" {
|
||||||
|
profile = "default"
|
||||||
|
}
|
||||||
|
details.Configuration["profile"] = &config.AttrConfig{Value: profile, Source: config.Source{Type: config.SourceDynamicConfig}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset source for databricks_cli_path because it can't be overridden anyway
|
||||||
|
if v, ok := details.Configuration["databricks_cli_path"]; ok {
|
||||||
|
v.Source = config.Source{Type: config.SourceDynamicConfig}
|
||||||
|
}
|
||||||
|
|
||||||
|
return details
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/cmd/root"
|
||||||
|
"github.com/databricks/databricks-sdk-go/config"
|
||||||
|
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/iam"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetWorkspaceAuthStatus(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
ctx = root.SetWorkspaceClient(ctx, m.WorkspaceClient)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
showSensitive := false
|
||||||
|
|
||||||
|
currentUserApi := m.GetMockCurrentUserAPI()
|
||||||
|
currentUserApi.EXPECT().Me(mock.Anything).Return(&iam.User{
|
||||||
|
UserName: "test-user",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
cmd.Flags().String("host", "", "")
|
||||||
|
cmd.Flags().String("profile", "", "")
|
||||||
|
cmd.Flag("profile").Value.Set("my-profile")
|
||||||
|
cmd.Flag("profile").Changed = true
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Profile: "my-profile",
|
||||||
|
}
|
||||||
|
m.WorkspaceClient.Config = cfg
|
||||||
|
t.Setenv("DATABRICKS_AUTH_TYPE", "azure-cli")
|
||||||
|
config.ConfigAttributes.Configure(cfg)
|
||||||
|
|
||||||
|
status, err := getAuthStatus(cmd, []string{}, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
|
||||||
|
config.ConfigAttributes.ResolveFromStringMap(cfg, map[string]string{
|
||||||
|
"host": "https://test.com",
|
||||||
|
"token": "test-token",
|
||||||
|
"auth_type": "azure-cli",
|
||||||
|
})
|
||||||
|
return cfg, false, nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, status)
|
||||||
|
require.Equal(t, "success", status.Status)
|
||||||
|
require.Equal(t, "test-user", status.Username)
|
||||||
|
require.Equal(t, "https://test.com", status.Details.Host)
|
||||||
|
require.Equal(t, "azure-cli", status.Details.AuthType)
|
||||||
|
|
||||||
|
require.Equal(t, "azure-cli", status.Details.Configuration["auth_type"].Value)
|
||||||
|
require.Equal(t, "DATABRICKS_AUTH_TYPE environment variable", status.Details.Configuration["auth_type"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["auth_type"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "********", status.Details.Configuration["token"].Value)
|
||||||
|
require.Equal(t, "dynamic configuration", status.Details.Configuration["token"].Source.String())
|
||||||
|
require.True(t, status.Details.Configuration["token"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "my-profile", status.Details.Configuration["profile"].Value)
|
||||||
|
require.Equal(t, "--profile flag", status.Details.Configuration["profile"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["profile"].AuthTypeMismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorkspaceAuthStatusError(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
ctx = root.SetWorkspaceClient(ctx, m.WorkspaceClient)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
showSensitive := false
|
||||||
|
|
||||||
|
cmd.Flags().String("host", "", "")
|
||||||
|
cmd.Flags().String("profile", "", "")
|
||||||
|
cmd.Flag("profile").Value.Set("my-profile")
|
||||||
|
cmd.Flag("profile").Changed = true
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Profile: "my-profile",
|
||||||
|
}
|
||||||
|
m.WorkspaceClient.Config = cfg
|
||||||
|
t.Setenv("DATABRICKS_AUTH_TYPE", "azure-cli")
|
||||||
|
config.ConfigAttributes.Configure(cfg)
|
||||||
|
|
||||||
|
status, err := getAuthStatus(cmd, []string{}, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
|
||||||
|
config.ConfigAttributes.ResolveFromStringMap(cfg, map[string]string{
|
||||||
|
"host": "https://test.com",
|
||||||
|
"token": "test-token",
|
||||||
|
"auth_type": "azure-cli",
|
||||||
|
})
|
||||||
|
return cfg, false, fmt.Errorf("auth error")
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, status)
|
||||||
|
require.Equal(t, "error", status.Status)
|
||||||
|
|
||||||
|
require.Equal(t, "azure-cli", status.Details.Configuration["auth_type"].Value)
|
||||||
|
require.Equal(t, "DATABRICKS_AUTH_TYPE environment variable", status.Details.Configuration["auth_type"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["auth_type"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "********", status.Details.Configuration["token"].Value)
|
||||||
|
require.Equal(t, "dynamic configuration", status.Details.Configuration["token"].Source.String())
|
||||||
|
require.True(t, status.Details.Configuration["token"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "my-profile", status.Details.Configuration["profile"].Value)
|
||||||
|
require.Equal(t, "--profile flag", status.Details.Configuration["profile"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["profile"].AuthTypeMismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorkspaceAuthStatusSensitive(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
ctx = root.SetWorkspaceClient(ctx, m.WorkspaceClient)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
showSensitive := true
|
||||||
|
|
||||||
|
cmd.Flags().String("host", "", "")
|
||||||
|
cmd.Flags().String("profile", "", "")
|
||||||
|
cmd.Flag("profile").Value.Set("my-profile")
|
||||||
|
cmd.Flag("profile").Changed = true
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Profile: "my-profile",
|
||||||
|
}
|
||||||
|
m.WorkspaceClient.Config = cfg
|
||||||
|
t.Setenv("DATABRICKS_AUTH_TYPE", "azure-cli")
|
||||||
|
config.ConfigAttributes.Configure(cfg)
|
||||||
|
|
||||||
|
status, err := getAuthStatus(cmd, []string{}, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
|
||||||
|
config.ConfigAttributes.ResolveFromStringMap(cfg, map[string]string{
|
||||||
|
"host": "https://test.com",
|
||||||
|
"token": "test-token",
|
||||||
|
"auth_type": "azure-cli",
|
||||||
|
})
|
||||||
|
return cfg, false, fmt.Errorf("auth error")
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, status)
|
||||||
|
require.Equal(t, "error", status.Status)
|
||||||
|
|
||||||
|
require.Equal(t, "azure-cli", status.Details.Configuration["auth_type"].Value)
|
||||||
|
require.Equal(t, "DATABRICKS_AUTH_TYPE environment variable", status.Details.Configuration["auth_type"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["auth_type"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "test-token", status.Details.Configuration["token"].Value)
|
||||||
|
require.Equal(t, "dynamic configuration", status.Details.Configuration["token"].Source.String())
|
||||||
|
require.True(t, status.Details.Configuration["token"].AuthTypeMismatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAccountAuthStatus(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
m := mocks.NewMockAccountClient(t)
|
||||||
|
ctx = root.SetAccountClient(ctx, m.AccountClient)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
showSensitive := false
|
||||||
|
|
||||||
|
cmd.Flags().String("host", "", "")
|
||||||
|
cmd.Flags().String("profile", "", "")
|
||||||
|
cmd.Flag("profile").Value.Set("my-profile")
|
||||||
|
cmd.Flag("profile").Changed = true
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Profile: "my-profile",
|
||||||
|
}
|
||||||
|
m.AccountClient.Config = cfg
|
||||||
|
t.Setenv("DATABRICKS_AUTH_TYPE", "azure-cli")
|
||||||
|
config.ConfigAttributes.Configure(cfg)
|
||||||
|
|
||||||
|
wsApi := m.GetMockWorkspacesAPI()
|
||||||
|
wsApi.EXPECT().List(mock.Anything).Return(nil, nil)
|
||||||
|
|
||||||
|
status, err := getAuthStatus(cmd, []string{}, showSensitive, func(cmd *cobra.Command, args []string) (*config.Config, bool, error) {
|
||||||
|
config.ConfigAttributes.ResolveFromStringMap(cfg, map[string]string{
|
||||||
|
"account_id": "test-account-id",
|
||||||
|
"username": "test-user",
|
||||||
|
"host": "https://test.com",
|
||||||
|
"token": "test-token",
|
||||||
|
"auth_type": "azure-cli",
|
||||||
|
})
|
||||||
|
return cfg, true, nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, status)
|
||||||
|
require.Equal(t, "success", status.Status)
|
||||||
|
|
||||||
|
require.Equal(t, "test-user", status.Username)
|
||||||
|
require.Equal(t, "https://test.com", status.Details.Host)
|
||||||
|
require.Equal(t, "azure-cli", status.Details.AuthType)
|
||||||
|
require.Equal(t, "test-account-id", status.AccountID)
|
||||||
|
|
||||||
|
require.Equal(t, "azure-cli", status.Details.Configuration["auth_type"].Value)
|
||||||
|
require.Equal(t, "DATABRICKS_AUTH_TYPE environment variable", status.Details.Configuration["auth_type"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["auth_type"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "********", status.Details.Configuration["token"].Value)
|
||||||
|
require.Equal(t, "dynamic configuration", status.Details.Configuration["token"].Source.String())
|
||||||
|
require.True(t, status.Details.Configuration["token"].AuthTypeMismatch)
|
||||||
|
|
||||||
|
require.Equal(t, "my-profile", status.Details.Configuration["profile"].Value)
|
||||||
|
require.Equal(t, "--profile flag", status.Details.Configuration["profile"].Source.String())
|
||||||
|
require.False(t, status.Details.Configuration["profile"].AuthTypeMismatch)
|
||||||
|
}
|
|
@ -17,6 +17,23 @@ import (
|
||||||
// Placeholders to use as unique keys in context.Context.
|
// Placeholders to use as unique keys in context.Context.
|
||||||
var workspaceClient int
|
var workspaceClient int
|
||||||
var accountClient int
|
var accountClient int
|
||||||
|
var configUsed int
|
||||||
|
|
||||||
|
type ErrNoWorkspaceProfiles struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrNoWorkspaceProfiles) Error() string {
|
||||||
|
return fmt.Sprintf("%s does not contain workspace profiles; please create one by running 'databricks configure'", e.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrNoAccountProfiles struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrNoAccountProfiles) Error() string {
|
||||||
|
return fmt.Sprintf("%s does not contain account profiles", e.path)
|
||||||
|
}
|
||||||
|
|
||||||
func initProfileFlag(cmd *cobra.Command) {
|
func initProfileFlag(cmd *cobra.Command) {
|
||||||
cmd.PersistentFlags().StringP("profile", "p", "", "~/.databrickscfg profile")
|
cmd.PersistentFlags().StringP("profile", "p", "", "~/.databrickscfg profile")
|
||||||
|
@ -67,6 +84,29 @@ func accountClientOrPrompt(ctx context.Context, cfg *config.Config, allowPrompt
|
||||||
return a, err
|
return a, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MustAnyClient(cmd *cobra.Command, args []string) (bool, error) {
|
||||||
|
// Try to create a workspace client
|
||||||
|
werr := MustWorkspaceClient(cmd, args)
|
||||||
|
if werr == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the error is other than "not a workspace client error" or "no workspace profiles",
|
||||||
|
// return it because configuration is for workspace client
|
||||||
|
// and we don't want to try to create an account client.
|
||||||
|
if !errors.Is(werr, databricks.ErrNotWorkspaceClient) && !errors.As(werr, &ErrNoWorkspaceProfiles{}) {
|
||||||
|
return false, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the config used is account client one, so try to create an account client
|
||||||
|
aerr := MustAccountClient(cmd, args)
|
||||||
|
if errors.As(aerr, &ErrNoAccountProfiles{}) {
|
||||||
|
return false, aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, aerr
|
||||||
|
}
|
||||||
|
|
||||||
func MustAccountClient(cmd *cobra.Command, args []string) error {
|
func MustAccountClient(cmd *cobra.Command, args []string) error {
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
|
|
||||||
|
@ -76,6 +116,10 @@ func MustAccountClient(cmd *cobra.Command, args []string) error {
|
||||||
cfg.Profile = profile
|
cfg.Profile = profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
ctx = context.WithValue(ctx, &configUsed, cfg)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
if cfg.Profile == "" {
|
if cfg.Profile == "" {
|
||||||
// account-level CLI was not really done before, so here are the assumptions:
|
// account-level CLI was not really done before, so here are the assumptions:
|
||||||
// 1. only admins will have account configured
|
// 1. only admins will have account configured
|
||||||
|
@ -98,7 +142,8 @@ func MustAccountClient(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.SetContext(context.WithValue(cmd.Context(), &accountClient, a))
|
ctx = context.WithValue(ctx, &accountClient, a)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,13 +191,20 @@ func MustWorkspaceClient(cmd *cobra.Command, args []string) error {
|
||||||
cfg.Profile = profile
|
cfg.Profile = profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
ctx = context.WithValue(ctx, &configUsed, cfg)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
// Try to load a bundle configuration if we're allowed to by the caller (see `./auth_options.go`).
|
// Try to load a bundle configuration if we're allowed to by the caller (see `./auth_options.go`).
|
||||||
if !shouldSkipLoadBundle(cmd.Context()) {
|
if !shouldSkipLoadBundle(cmd.Context()) {
|
||||||
b, diags := TryConfigureBundle(cmd)
|
b, diags := TryConfigureBundle(cmd)
|
||||||
if err := diags.Error(); err != nil {
|
if err := diags.Error(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if b != nil {
|
if b != nil {
|
||||||
|
ctx = context.WithValue(ctx, &configUsed, b.Config.Workspace.Config())
|
||||||
|
cmd.SetContext(ctx)
|
||||||
client, err := b.InitializeWorkspaceClient()
|
client, err := b.InitializeWorkspaceClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -167,7 +219,6 @@ func MustWorkspaceClient(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := cmd.Context()
|
|
||||||
ctx = context.WithValue(ctx, &workspaceClient, w)
|
ctx = context.WithValue(ctx, &workspaceClient, w)
|
||||||
cmd.SetContext(ctx)
|
cmd.SetContext(ctx)
|
||||||
return nil
|
return nil
|
||||||
|
@ -177,6 +228,10 @@ func SetWorkspaceClient(ctx context.Context, w *databricks.WorkspaceClient) cont
|
||||||
return context.WithValue(ctx, &workspaceClient, w)
|
return context.WithValue(ctx, &workspaceClient, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetAccountClient(ctx context.Context, a *databricks.AccountClient) context.Context {
|
||||||
|
return context.WithValue(ctx, &accountClient, a)
|
||||||
|
}
|
||||||
|
|
||||||
func AskForWorkspaceProfile(ctx context.Context) (string, error) {
|
func AskForWorkspaceProfile(ctx context.Context) (string, error) {
|
||||||
path, err := databrickscfg.GetPath(ctx)
|
path, err := databrickscfg.GetPath(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -188,7 +243,7 @@ func AskForWorkspaceProfile(ctx context.Context) (string, error) {
|
||||||
}
|
}
|
||||||
switch len(profiles) {
|
switch len(profiles) {
|
||||||
case 0:
|
case 0:
|
||||||
return "", fmt.Errorf("%s does not contain workspace profiles; please create one by running 'databricks configure'", path)
|
return "", ErrNoWorkspaceProfiles{path: path}
|
||||||
case 1:
|
case 1:
|
||||||
return profiles[0].Name, nil
|
return profiles[0].Name, nil
|
||||||
}
|
}
|
||||||
|
@ -221,7 +276,7 @@ func AskForAccountProfile(ctx context.Context) (string, error) {
|
||||||
}
|
}
|
||||||
switch len(profiles) {
|
switch len(profiles) {
|
||||||
case 0:
|
case 0:
|
||||||
return "", fmt.Errorf("%s does not contain account profiles; please create one by running 'databricks configure'", path)
|
return "", ErrNoAccountProfiles{path}
|
||||||
case 1:
|
case 1:
|
||||||
return profiles[0].Name, nil
|
return profiles[0].Name, nil
|
||||||
}
|
}
|
||||||
|
@ -269,3 +324,11 @@ func AccountClient(ctx context.Context) *databricks.AccountClient {
|
||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConfigUsed(ctx context.Context) *config.Config {
|
||||||
|
cfg, ok := ctx.Value(&configUsed).(*config.Config)
|
||||||
|
if !ok {
|
||||||
|
panic("cannot get *config.Config. Please report it as a bug")
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
|
@ -229,3 +229,81 @@ func TestMustAccountClientErrorsWithNoDatabricksCfg(t *testing.T) {
|
||||||
err := MustAccountClient(cmd, []string{})
|
err := MustAccountClient(cmd, []string{})
|
||||||
require.ErrorContains(t, err, "no configuration file found at")
|
require.ErrorContains(t, err, "no configuration file found at")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMustAnyClientCanCreateWorkspaceClient(t *testing.T) {
|
||||||
|
testutil.CleanupEnvironment(t)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := filepath.Join(dir, ".databrickscfg")
|
||||||
|
err := os.WriteFile(
|
||||||
|
configFile,
|
||||||
|
[]byte(`
|
||||||
|
[workspace-1111]
|
||||||
|
host = https://adb-1111.11.azuredatabricks.net/
|
||||||
|
token = foobar
|
||||||
|
`),
|
||||||
|
0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, tt := cmdio.SetupTest(context.Background())
|
||||||
|
t.Cleanup(tt.Done)
|
||||||
|
cmd := New(ctx)
|
||||||
|
|
||||||
|
t.Setenv("DATABRICKS_CONFIG_FILE", configFile)
|
||||||
|
isAccount, err := MustAnyClient(cmd, []string{})
|
||||||
|
require.False(t, isAccount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w := WorkspaceClient(cmd.Context())
|
||||||
|
require.NotNil(t, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustAnyClientCanCreateAccountClient(t *testing.T) {
|
||||||
|
testutil.CleanupEnvironment(t)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := filepath.Join(dir, ".databrickscfg")
|
||||||
|
err := os.WriteFile(
|
||||||
|
configFile,
|
||||||
|
[]byte(`
|
||||||
|
[account-1111]
|
||||||
|
host = https://accounts.azuredatabricks.net/
|
||||||
|
account_id = 1111
|
||||||
|
token = foobar
|
||||||
|
`),
|
||||||
|
0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, tt := cmdio.SetupTest(context.Background())
|
||||||
|
t.Cleanup(tt.Done)
|
||||||
|
cmd := New(ctx)
|
||||||
|
|
||||||
|
t.Setenv("DATABRICKS_CONFIG_FILE", configFile)
|
||||||
|
isAccount, err := MustAnyClient(cmd, []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, isAccount)
|
||||||
|
|
||||||
|
a := AccountClient(cmd.Context())
|
||||||
|
require.NotNil(t, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustAnyClientWithEmptyDatabricksCfg(t *testing.T) {
|
||||||
|
testutil.CleanupEnvironment(t)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := filepath.Join(dir, ".databrickscfg")
|
||||||
|
err := os.WriteFile(
|
||||||
|
configFile,
|
||||||
|
[]byte(""), // empty file
|
||||||
|
0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, tt := cmdio.SetupTest(context.Background())
|
||||||
|
t.Cleanup(tt.Done)
|
||||||
|
cmd := New(ctx)
|
||||||
|
|
||||||
|
t.Setenv("DATABRICKS_CONFIG_FILE", configFile)
|
||||||
|
|
||||||
|
_, err = MustAnyClient(cmd, []string{})
|
||||||
|
require.ErrorContains(t, err, "does not contain account profiles")
|
||||||
|
}
|
||||||
|
|
|
@ -306,6 +306,12 @@ func renderUsingTemplate(ctx context.Context, r templateRenderer, w io.Writer, h
|
||||||
"yellow": color.YellowString,
|
"yellow": color.YellowString,
|
||||||
"magenta": color.MagentaString,
|
"magenta": color.MagentaString,
|
||||||
"cyan": color.CyanString,
|
"cyan": color.CyanString,
|
||||||
|
"bold": func(format string, a ...interface{}) string {
|
||||||
|
return color.New(color.Bold).Sprintf(format, a...)
|
||||||
|
},
|
||||||
|
"italic": func(format string, a ...interface{}) string {
|
||||||
|
return color.New(color.Italic).Sprintf(format, a...)
|
||||||
|
},
|
||||||
"replace": strings.ReplaceAll,
|
"replace": strings.ReplaceAll,
|
||||||
"join": strings.Join,
|
"join": strings.Join,
|
||||||
"bool": func(v bool) string {
|
"bool": func(v bool) string {
|
||||||
|
|
|
@ -98,7 +98,10 @@ func (l profileFromHostLoader) Configure(cfg *config.Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf(ctx, "Loading profile %s because of host match", match.Name())
|
log.Debugf(ctx, "Loading profile %s because of host match", match.Name())
|
||||||
err = config.ConfigAttributes.ResolveFromStringMap(cfg, match.KeysHash())
|
err = config.ConfigAttributes.ResolveFromStringMapWithSource(cfg, match.KeysHash(), config.Source{
|
||||||
|
Type: config.SourceFile,
|
||||||
|
Name: configFile.Path(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s %s profile: %w", configFile.Path(), match.Name(), err)
|
return fmt.Errorf("%s %s profile: %w", configFile.Path(), match.Name(), err)
|
||||||
}
|
}
|
||||||
|
@ -110,7 +113,7 @@ func (l profileFromHostLoader) Configure(cfg *config.Config) error {
|
||||||
func (l profileFromHostLoader) isAnyAuthConfigured(cfg *config.Config) bool {
|
func (l profileFromHostLoader) isAnyAuthConfigured(cfg *config.Config) bool {
|
||||||
// If any of the auth-specific attributes are set, we can skip profile resolution.
|
// If any of the auth-specific attributes are set, we can skip profile resolution.
|
||||||
for _, a := range config.ConfigAttributes {
|
for _, a := range config.ConfigAttributes {
|
||||||
if a.Auth == "" {
|
if !a.HasAuthAttribute() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !a.IsZero(cfg) {
|
if !a.IsZero(cfg) {
|
||||||
|
|
Loading…
Reference in New Issue