Skip to content

Commit 2f38bce

Browse files
author
Chen Shou
committed
Configure Docker authentication for Artifact Registry
1 parent 3ad7d11 commit 2f38bce

17 files changed

Lines changed: 1779 additions & 2 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `databricks auth configure-docker` to configure Docker credential helper access for Databricks Artifact Registry.

acceptance/cmd/auth/configure-docker-help/out.test.toml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
>>> [CLI] auth configure-docker --help
3+
Configure Docker authentication for Databricks Artifact Registry.
4+
5+
This command installs docker-credential-databricks and configures Docker to use
6+
it for the selected workspace's Artifact Registry host. If the selected profile
7+
does not already include a workspace_id, the command resolves and saves it so
8+
the Docker helper can map the registry host back to the profile. The required
9+
region must match the workspace home region because it cannot be inferred from
10+
the profile. Select the workspace with [PROFILE] or --profile; --host,
11+
--account-id, and --workspace-id are not supported.
12+
13+
Usage:
14+
databricks auth configure-docker [PROFILE] --region REGION [flags]
15+
16+
Flags:
17+
-h, --help help for configure-docker
18+
--region string Cloud region for the Databricks Artifact Registry host; must match the workspace home region
19+
20+
Global Flags:
21+
--account-id string Databricks Account ID
22+
--debug enable debug logging
23+
--host string Databricks Host
24+
-o, --output type output type: text or json (default text)
25+
-p, --profile string ~/.databrickscfg profile
26+
-t, --target string bundle target to use (if applicable)
27+
--workspace-id string Databricks Workspace ID
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
trace $CLI auth configure-docker --help
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

cmd/auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ GCP: https://docs.gcp.databricks.com/dev-tools/auth/index.html`,
3535
cmd.AddCommand(newLogoutCommand())
3636
cmd.AddCommand(newProfilesCommand())
3737
cmd.AddCommand(newTokenCommand(&authArguments))
38+
cmd.AddCommand(newConfigureDockerCommand())
3839
cmd.AddCommand(newDescribeCommand())
3940
cmd.AddCommand(newSwitchCommand())
4041
return cmd

cmd/auth/configure_docker.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
authlib "github.com/databricks/cli/libs/auth"
12+
"github.com/databricks/cli/libs/cmdio"
13+
"github.com/databricks/cli/libs/databrickscfg"
14+
"github.com/databricks/cli/libs/databrickscfg/profile"
15+
"github.com/databricks/cli/libs/dockercredentials"
16+
"github.com/databricks/cli/libs/env"
17+
"github.com/databricks/databricks-sdk-go"
18+
"github.com/databricks/databricks-sdk-go/config"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
// configureDockerDeps groups injectable profile reads, workspace resolution, executable discovery, and Docker operations.
23+
type configureDockerDeps struct {
24+
profiler profile.Profiler
25+
newWorkspaceClient func(*databricks.Config) (*databricks.WorkspaceClient, error)
26+
resolveWorkspaceID func(context.Context, *databricks.WorkspaceClient) (string, error)
27+
executable func() (string, error)
28+
registryHost func(string, string, string) (string, error)
29+
installShim func(string, string) (dockercredentials.ShimInstallResult, error)
30+
setCredentialHelper func(string, string) error
31+
}
32+
33+
// defaultConfigureDockerDeps provides production implementations for the command's injectable dependencies.
34+
func defaultConfigureDockerDeps() configureDockerDeps {
35+
return configureDockerDeps{
36+
profiler: profile.DefaultProfiler,
37+
newWorkspaceClient: func(cfg *databricks.Config) (*databricks.WorkspaceClient, error) {
38+
return databricks.NewWorkspaceClient(cfg)
39+
},
40+
resolveWorkspaceID: authlib.ResolveWorkspaceID,
41+
executable: os.Executable,
42+
registryHost: dockercredentials.RegistryHost,
43+
installShim: dockercredentials.InstallShim,
44+
setCredentialHelper: dockercredentials.SetCredentialHelper,
45+
}
46+
}
47+
48+
// newConfigureDockerCommand is the production entry point; tests use the dependency-injected constructor.
49+
func newConfigureDockerCommand() *cobra.Command {
50+
return newConfigureDockerCommandWithDeps(defaultConfigureDockerDeps())
51+
}
52+
53+
// newConfigureDockerCommandWithDeps accepts replacements for profile reads, workspace resolution, and Docker operations.
54+
func newConfigureDockerCommandWithDeps(deps configureDockerDeps) *cobra.Command {
55+
cmd := &cobra.Command{
56+
Use: "configure-docker [PROFILE] --region REGION",
57+
Short: "Configure Docker authentication for Databricks Artifact Registry",
58+
Long: `Configure Docker authentication for Databricks Artifact Registry.
59+
60+
This command installs docker-credential-databricks and configures Docker to use
61+
it for the selected workspace's Artifact Registry host. If the selected profile
62+
does not already include a workspace_id, the command resolves and saves it so
63+
the Docker helper can map the registry host back to the profile. The required
64+
region must match the workspace home region because it cannot be inferred from
65+
the profile. Select the workspace with [PROFILE] or --profile; --host,
66+
--account-id, and --workspace-id are not supported.`,
67+
Args: cobra.MaximumNArgs(1),
68+
}
69+
var region string
70+
cmd.Flags().StringVar(&region, "region", "", "Cloud region for the Databricks Artifact Registry host; must match the workspace home region")
71+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
72+
ctx := cmd.Context()
73+
if err := errorOnUnsupportedConfigureDockerFlags(cmd); err != nil {
74+
return err
75+
}
76+
// Workspace profiles do not expose the home region needed for the registry hostname.
77+
if region == "" {
78+
return errors.New("--region is required because workspace region cannot be inferred from this profile; it must match the workspace home region")
79+
}
80+
81+
profileName, err := configureDockerProfileName(ctx, cmd, args, deps.profiler)
82+
if err != nil {
83+
return err
84+
}
85+
86+
p, err := loadAndValidateConfigureDockerProfile(ctx, profileName, deps.profiler)
87+
if err != nil {
88+
return err
89+
}
90+
91+
executable, err := deps.executable()
92+
if err != nil {
93+
return fmt.Errorf("locate databricks executable: %w", err)
94+
}
95+
workspaceID, err := resolveConfigureDockerWorkspaceID(ctx, p, executable, deps)
96+
if err != nil {
97+
return err
98+
}
99+
// The workspace host supplies the cloud and environment DNS zone for the registry hostname.
100+
registryHost, err := deps.registryHost(workspaceID, region, p.Host)
101+
if err != nil {
102+
return err
103+
}
104+
if err := ensureConfigureDockerUniqueProfile(ctx, deps.profiler, p, workspaceID, region, registryHost, deps.registryHost); err != nil {
105+
return err
106+
}
107+
if p.WorkspaceID == "" || p.WorkspaceID == authlib.WorkspaceIDNone {
108+
if err := persistConfigureDockerWorkspaceID(ctx, p, workspaceID); err != nil {
109+
return fmt.Errorf("save workspace ID to profile %q: %w", p.Name, err)
110+
}
111+
}
112+
113+
// Installing beside this CLI lets an existing PATH entry discover both executables.
114+
installDir := filepath.Dir(executable)
115+
shim, err := deps.installShim(executable, installDir)
116+
if err != nil {
117+
return fmt.Errorf("install Docker credential helper: %w", err)
118+
}
119+
dockerConfigPath, err := configureDockerConfigPath(ctx)
120+
if err != nil {
121+
return err
122+
}
123+
if err := deps.setCredentialHelper(dockerConfigPath, registryHost); err != nil {
124+
return fmt.Errorf("update Docker config %s: %w", dockerConfigPath, err)
125+
}
126+
127+
cmdio.LogString(ctx, "Configured Docker credential helper for "+registryHost)
128+
cmdio.LogString(ctx, "Updated Docker config: "+dockerConfigPath)
129+
cmdio.LogString(ctx, "Installed Docker credential helper: "+shim.Path)
130+
if !shim.OnPath {
131+
cmdio.LogString(ctx, fmt.Sprintf("Warning: ensure %s is on PATH before any other docker-credential-databricks helper, and that .EXE is in PATHEXT on Windows", installDir))
132+
}
133+
return nil
134+
}
135+
136+
return cmd
137+
}
138+
139+
// errorOnUnsupportedConfigureDockerFlags rejects inherited selectors that bypass the durable profile-to-registry mapping.
140+
func errorOnUnsupportedConfigureDockerFlags(cmd *cobra.Command) error {
141+
for _, name := range []string{"host", "account-id", "workspace-id"} {
142+
flag := cmd.Flag(name)
143+
if flag != nil && flag.Changed {
144+
return fmt.Errorf("--%s is not supported for configure-docker. Select the workspace with [PROFILE] or --profile instead", name)
145+
}
146+
}
147+
return nil
148+
}
149+
150+
// configureDockerProfileName resolves an explicit profile before environment, default-profile, and interactive selection.
151+
func configureDockerProfileName(ctx context.Context, cmd *cobra.Command, args []string, profiler profile.Profiler) (string, error) {
152+
profileFlag := cmd.Flag("profile")
153+
profileName := ""
154+
if profileFlag != nil {
155+
profileName = profileFlag.Value.String()
156+
}
157+
if len(args) == 1 {
158+
if profileName != "" {
159+
return "", fmt.Errorf("argument %q cannot be combined with --profile. Use --profile instead", args[0])
160+
}
161+
return args[0], nil
162+
}
163+
if profileName != "" {
164+
return profileName, nil
165+
}
166+
if profileName = env.Get(ctx, "DATABRICKS_CONFIG_PROFILE"); profileName != "" {
167+
return profileName, nil
168+
}
169+
if profileName = databrickscfg.ResolveDefaultProfile(ctx); profileName != "" {
170+
return profileName, nil
171+
}
172+
if !cmdio.IsPromptSupported(ctx) {
173+
return "", errors.New("no profile specified. Use --profile <name> to specify which profile to use")
174+
}
175+
176+
profiles, err := profiler.LoadProfiles(ctx, profile.MatchWorkspaceProfiles)
177+
if err != nil {
178+
return "", err
179+
}
180+
currentDefault, _ := databrickscfg.GetDefaultProfile(ctx, env.Get(ctx, "DATABRICKS_CONFIG_FILE"))
181+
result, selected, err := pickAuthProfile(ctx, profiles, profilePickerOptions{
182+
Label: "Select a workspace profile",
183+
Default: currentDefault,
184+
})
185+
if err != nil {
186+
return "", err
187+
}
188+
if result != profilePickerProfile {
189+
return "", errors.New("no profile selected")
190+
}
191+
return selected, nil
192+
}
193+
194+
// loadAndValidateConfigureDockerProfile loads one named profile and checks that its metadata is eligible for workspace U2M authentication.
195+
func loadAndValidateConfigureDockerProfile(ctx context.Context, profileName string, profiler profile.Profiler) (profile.Profile, error) {
196+
profiles, err := profiler.LoadProfiles(ctx, profile.WithName(profileName))
197+
if err != nil {
198+
return profile.Profile{}, err
199+
}
200+
if len(profiles) == 0 {
201+
return profile.Profile{}, fmt.Errorf("profile %q not found", profileName)
202+
}
203+
if err := validateDockerCredentialProfile(profiles[0]); err != nil {
204+
return profile.Profile{}, err
205+
}
206+
return profiles[0], nil
207+
}
208+
209+
// resolveConfigureDockerWorkspaceID queries /Me when the profile has no usable ID without allowing ambient routing or the "none" sentinel into the request.
210+
func resolveConfigureDockerWorkspaceID(ctx context.Context, p profile.Profile, executable string, deps configureDockerDeps) (string, error) {
211+
if p.WorkspaceID != "" && p.WorkspaceID != authlib.WorkspaceIDNone {
212+
return p.WorkspaceID, nil
213+
}
214+
215+
cfg := &databricks.Config{
216+
Profile: p.Name,
217+
Host: p.Host,
218+
AccountID: p.AccountID,
219+
AuthType: p.AuthType,
220+
ConfigFile: env.Get(ctx, "DATABRICKS_CONFIG_FILE"),
221+
Loaders: databrickscfg.ProfileAuthLoaders,
222+
DatabricksCliPath: executable,
223+
}
224+
w, err := deps.newWorkspaceClient(cfg)
225+
if err != nil {
226+
return "", fmt.Errorf("load workspace profile %q: %w. Run databricks auth login --host <workspace-url> and retry with that profile", p.Name, err)
227+
}
228+
// The selected profile may contain the CLI-only "none" sentinel, which the SDK would send as a routing header.
229+
w.Config.WorkspaceID = ""
230+
workspaceID, err := deps.resolveWorkspaceID(ctx, w)
231+
if err != nil {
232+
return "", fmt.Errorf("resolve workspace ID for profile %q: %w. Run databricks auth login --host <workspace-url> and retry with that profile", p.Name, err)
233+
}
234+
return workspaceID, nil
235+
}
236+
237+
// ensureConfigureDockerUniqueProfile rejects profiles that resolve to the same registry host because workspace IDs can repeat across environments.
238+
func ensureConfigureDockerUniqueProfile(ctx context.Context, profiler profile.Profiler, p profile.Profile, workspaceID, region, selectedRegistryHost string, registryHost registryHostResolver) error {
239+
matches, err := profiler.LoadProfiles(ctx, func(candidate profile.Profile) bool {
240+
return candidate.WorkspaceID == workspaceID
241+
})
242+
if err != nil {
243+
return err
244+
}
245+
246+
var names []string
247+
for _, candidate := range matches {
248+
if validateDockerCredentialProfile(candidate) != nil {
249+
continue
250+
}
251+
candidateRegistryHost, err := registryHost(workspaceID, region, candidate.Host)
252+
if err == nil && candidateRegistryHost == selectedRegistryHost {
253+
names = append(names, candidate.Name)
254+
}
255+
}
256+
if p.WorkspaceID == "" || p.WorkspaceID == authlib.WorkspaceIDNone {
257+
names = append(names, p.Name)
258+
}
259+
if len(names) <= 1 {
260+
return nil
261+
}
262+
263+
return fmt.Errorf("multiple Databricks profiles match workspace ID %s: %s. Remove duplicate workspace_id entries before using Docker credential helper", workspaceID, strings.Join(names, " and "))
264+
}
265+
266+
// persistConfigureDockerWorkspaceID adds the resolved ID to the selected profile without replacing its other settings.
267+
func persistConfigureDockerWorkspaceID(ctx context.Context, p profile.Profile, workspaceID string) error {
268+
return databrickscfg.SaveToProfile(ctx, &config.Config{
269+
ConfigFile: env.Get(ctx, "DATABRICKS_CONFIG_FILE"),
270+
Profile: p.Name,
271+
WorkspaceID: workspaceID,
272+
})
273+
}
274+
275+
// configureDockerConfigPath honors Docker's DOCKER_CONFIG override before the per-user default.
276+
// See https://docs.docker.com/reference/cli/docker/#configuration-files.
277+
func configureDockerConfigPath(ctx context.Context) (string, error) {
278+
if dockerConfig := env.Get(ctx, "DOCKER_CONFIG"); dockerConfig != "" {
279+
return filepath.Join(dockerConfig, "config.json"), nil
280+
}
281+
home, err := env.UserHomeDir(ctx)
282+
if err != nil {
283+
return "", err
284+
}
285+
return filepath.Join(home, ".docker", "config.json"), nil
286+
}

0 commit comments

Comments
 (0)