Skip to content

Commit fcfc26d

Browse files
committed
Refactor auth env to use CLI's resolved auth context
Use root.MustAnyClient and auth.Env instead of custom profile/host resolution logic. This makes auth env return the environment variables for the exact identity the CLI is authenticated as, including bundle context and all standard auth resolution paths. Breaking changes: - Removed command-specific --host and --profile flags (use the inherited flags from the parent/root commands) - JSON output is a flat map instead of wrapped in {"env": ...} - Only the primary env var per attribute is emitted (via auth.Env)
1 parent 23d833e commit fcfc26d

File tree

2 files changed

+30
-143
lines changed

2 files changed

+30
-143
lines changed

cmd/auth/env.go

Lines changed: 19 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,48 @@
11
package auth
22

33
import (
4-
"context"
54
"encoding/json"
6-
"errors"
75
"fmt"
8-
"io/fs"
9-
"net/http"
10-
"net/url"
6+
"maps"
7+
"slices"
118
"strings"
129

1310
"github.com/databricks/cli/cmd/root"
14-
"github.com/databricks/cli/libs/databrickscfg/profile"
11+
"github.com/databricks/cli/libs/auth"
12+
"github.com/databricks/cli/libs/cmdctx"
1513
"github.com/databricks/cli/libs/flags"
16-
"github.com/databricks/databricks-sdk-go/config"
1714
"github.com/spf13/cobra"
18-
"gopkg.in/ini.v1"
1915
)
2016

21-
func canonicalHost(host string) (string, error) {
22-
parsedHost, err := url.Parse(host)
23-
if err != nil {
24-
return "", err
25-
}
26-
// If the host is empty, assume the scheme wasn't included.
27-
if parsedHost.Host == "" {
28-
return "https://" + host, nil
29-
}
30-
return "https://" + parsedHost.Host, nil
31-
}
32-
33-
var ErrNoMatchingProfiles = errors.New("no matching profiles found")
34-
35-
const shellQuotedSpecialChars = " \t\n\r\"\\$`!#&|;(){}[]<>?*~'"
36-
37-
func resolveSection(cfg *config.Config, iniFile *config.File) (*ini.Section, error) {
38-
var candidates []*ini.Section
39-
configuredHost, err := canonicalHost(cfg.Host)
40-
if err != nil {
41-
return nil, err
42-
}
43-
for _, section := range iniFile.Sections() {
44-
hash := section.KeysHash()
45-
host, ok := hash["host"]
46-
if !ok {
47-
// if host is not set
48-
continue
49-
}
50-
canonical, err := canonicalHost(host)
51-
if err != nil {
52-
// we're fine with other corrupt profiles
53-
continue
54-
}
55-
if canonical != configuredHost {
56-
continue
57-
}
58-
candidates = append(candidates, section)
59-
}
60-
if len(candidates) == 0 {
61-
return nil, ErrNoMatchingProfiles
62-
}
63-
// in the real situations, we don't expect this to happen often
64-
// (if not at all), hence we don't trim the list
65-
if len(candidates) > 1 {
66-
var profiles []string
67-
for _, v := range candidates {
68-
profiles = append(profiles, v.Name())
69-
}
70-
return nil, fmt.Errorf("%s match %s in %s",
71-
strings.Join(profiles, " and "), cfg.Host, cfg.ConfigFile)
72-
}
73-
return candidates[0], nil
74-
}
75-
76-
func loadFromDatabricksCfg(ctx context.Context, cfg *config.Config) error {
77-
iniFile, err := profile.DefaultProfiler.Get(ctx)
78-
if errors.Is(err, fs.ErrNotExist) {
79-
// it's fine not to have ~/.databrickscfg
80-
return nil
81-
}
82-
if err != nil {
83-
return err
84-
}
85-
profile, err := resolveSection(cfg, iniFile)
86-
if err == ErrNoMatchingProfiles {
87-
// it's also fine for Azure CLI or Databricks CLI, which
88-
// are resolved by unified auth handling in the Go SDK.
89-
return nil
90-
}
91-
if err != nil {
92-
return err
93-
}
94-
cfg.Profile = profile.Name()
95-
return nil
96-
}
97-
9817
func newEnvCommand() *cobra.Command {
9918
cmd := &cobra.Command{
10019
Use: "env",
101-
Short: "Get env",
20+
Short: "Get authentication environment variables for the current CLI context",
21+
Long: `Output the environment variables needed to authenticate as the same identity
22+
the CLI is currently authenticated as. This is useful for configuring downstream
23+
tools that accept Databricks authentication via environment variables.`,
10224
}
10325

104-
var host string
105-
var profile string
106-
cmd.Flags().StringVar(&host, "host", host, "Hostname to get auth env for")
107-
cmd.Flags().StringVar(&profile, "profile", profile, "Profile to get auth env for")
108-
10926
cmd.RunE = func(cmd *cobra.Command, args []string) error {
110-
cfg := &config.Config{
111-
Host: host,
112-
Profile: profile,
113-
}
114-
if profile != "" {
115-
cfg.Profile = profile
116-
} else if cfg.Host == "" {
117-
cfg.Profile = "DEFAULT"
118-
} else if err := loadFromDatabricksCfg(cmd.Context(), cfg); err != nil {
119-
return err
120-
}
121-
// Go SDK is lazy loaded because of Terraform semantics,
122-
// so we're creating a dummy HTTP request as a placeholder
123-
// for headers.
124-
r := &http.Request{Header: http.Header{}}
125-
err := cfg.Authenticate(r.WithContext(cmd.Context()))
27+
_, err := root.MustAnyClient(cmd, args)
12628
if err != nil {
12729
return err
12830
}
31+
32+
cfg := cmdctx.ConfigUsed(cmd.Context())
33+
envVars := auth.Env(cfg)
34+
12935
// Output KEY=VALUE lines when the user explicitly passes --output text.
13036
if cmd.Flag("output").Changed && root.OutputType(cmd) == flags.OutputText {
13137
w := cmd.OutOrStdout()
132-
for _, a := range config.ConfigAttributes {
133-
if a.IsZero(cfg) {
134-
continue
135-
}
136-
v := a.GetString(cfg)
137-
for _, envName := range a.EnvVars {
138-
fmt.Fprintf(w, "%s=%s\n", envName, quoteEnvValue(v))
139-
}
38+
keys := slices.Sorted(maps.Keys(envVars))
39+
for _, k := range keys {
40+
fmt.Fprintf(w, "%s=%s\n", k, quoteEnvValue(envVars[k]))
14041
}
14142
return nil
14243
}
14344

144-
vars := collectEnvVars(cfg)
145-
raw, err := json.MarshalIndent(map[string]any{
146-
"env": vars,
147-
}, "", " ")
45+
raw, err := json.MarshalIndent(envVars, "", " ")
14846
if err != nil {
14947
return err
15048
}
@@ -155,25 +53,11 @@ func newEnvCommand() *cobra.Command {
15553
return cmd
15654
}
15755

158-
// collectEnvVars returns the environment variables for the given config
159-
// as a map from env var name to value.
160-
func collectEnvVars(cfg *config.Config) map[string]string {
161-
vars := map[string]string{}
162-
for _, a := range config.ConfigAttributes {
163-
if a.IsZero(cfg) {
164-
continue
165-
}
166-
v := a.GetString(cfg)
167-
for _, envName := range a.EnvVars {
168-
vars[envName] = v
169-
}
170-
}
171-
return vars
172-
}
56+
const shellQuotedSpecialChars = " \t\n\r\"\\$`!#&|;(){}[]<>?*~'"
17357

17458
// quoteEnvValue quotes a value for KEY=VALUE output if it contains spaces or
17559
// shell-special characters. Single quotes prevent shell expansion, and
176-
// embedded single quotes use the POSIX-compatible '\ sequence.
60+
// embedded single quotes use the POSIX-compatible '\" sequence.
17761
func quoteEnvValue(v string) string {
17862
if v == "" {
17963
return `''`

cmd/auth/env_test.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,39 @@ func TestEnvCommand_TextOutput(t *testing.T) {
4747
}{
4848
{
4949
name: "default output is JSON",
50-
args: []string{"--host", "https://test.cloud.databricks.com"},
50+
args: nil,
5151
wantJSON: true,
5252
},
5353
{
5454
name: "explicit --output text produces KEY=VALUE lines",
55-
args: []string{"--host", "https://test.cloud.databricks.com", "--output", "text"},
55+
args: []string{"--output", "text"},
5656
wantJSON: false,
5757
},
5858
{
5959
name: "explicit --output json produces JSON",
60-
args: []string{"--host", "https://test.cloud.databricks.com", "--output", "json"},
60+
args: []string{"--output", "json"},
6161
wantJSON: true,
6262
},
6363
}
6464

6565
for _, c := range cases {
6666
t.Run(c.name, func(t *testing.T) {
67+
// Isolate from real config/token cache on the machine.
68+
t.Setenv("DATABRICKS_CONFIG_FILE", t.TempDir()+"/.databrickscfg")
69+
t.Setenv("HOME", t.TempDir())
70+
// Set env vars so MustAnyClient resolves auth via PAT.
71+
t.Setenv("DATABRICKS_HOST", "https://test.cloud.databricks.com")
72+
t.Setenv("DATABRICKS_TOKEN", "test-token-value")
73+
6774
parent := &cobra.Command{Use: "databricks"}
6875
outputFlag := flags.OutputText
6976
parent.PersistentFlags().VarP(&outputFlag, "output", "o", "output type: text or json")
77+
parent.PersistentFlags().StringP("profile", "p", "", "~/.databrickscfg profile")
7078

7179
envCmd := newEnvCommand()
7280
parent.AddCommand(envCmd)
7381
parent.SetContext(cmdio.MockDiscard(t.Context()))
7482

75-
// Set DATABRICKS_TOKEN so the SDK's config.Authenticate succeeds
76-
// without hitting a real endpoint.
77-
t.Setenv("DATABRICKS_TOKEN", "test-token-value")
78-
7983
var buf bytes.Buffer
8084
parent.SetOut(&buf)
8185
parent.SetArgs(append([]string{"env"}, c.args...))
@@ -91,7 +95,6 @@ func TestEnvCommand_TextOutput(t *testing.T) {
9195
assert.NotContains(t, output, "{")
9296
assert.Contains(t, output, "DATABRICKS_HOST=")
9397
assert.Contains(t, output, "=")
94-
// Verify KEY=VALUE format (no JSON structure)
9598
assert.NotContains(t, output, `"env"`)
9699
}
97100
})

0 commit comments

Comments
 (0)