diff --git a/cmd/cli/commands/compose.go b/cmd/cli/commands/compose.go index 7d10a6d27..552eb9ee1 100644 --- a/cmd/cli/commands/compose.go +++ b/cmd/cli/commands/compose.go @@ -63,6 +63,7 @@ func newUpCommand() *cobra.Command { return errors.New("unable to determine standalone runner endpoint") } + syncDockerConfigForRegistry(cmd.Context(), asPrinter(cmd)) if err := downloadModelsOnlyIfNotFound(desktopClient, models); err != nil { return err } diff --git a/cmd/cli/commands/pull.go b/cmd/cli/commands/pull.go index 6de1676ff..1a163fe69 100644 --- a/cmd/cli/commands/pull.go +++ b/cmd/cli/commands/pull.go @@ -22,6 +22,7 @@ func newPullCmd() *cobra.Command { func pullModel(cmd *cobra.Command, desktopClient *desktop.Client, model string) error { printer := asPrinter(cmd) + syncDockerConfigForRegistry(cmd.Context(), printer) response, _, err := desktopClient.Pull(model, printer) if err != nil { diff --git a/cmd/cli/commands/push.go b/cmd/cli/commands/push.go index ef9c0b91f..f13b2adea 100644 --- a/cmd/cli/commands/push.go +++ b/cmd/cli/commands/push.go @@ -21,6 +21,7 @@ func newPushCmd() *cobra.Command { func pushModel(cmd *cobra.Command, desktopClient *desktop.Client, model string) error { printer := asPrinter(cmd) + syncDockerConfigForRegistry(cmd.Context(), printer) response, _, err := desktopClient.Push(model, printer) if err != nil { diff --git a/cmd/cli/commands/utils.go b/cmd/cli/commands/utils.go index fbe1a932e..39bdf0d4e 100644 --- a/cmd/cli/commands/utils.go +++ b/cmd/cli/commands/utils.go @@ -2,6 +2,7 @@ package commands import ( "bytes" + "context" "errors" "fmt" "io" @@ -10,6 +11,7 @@ import ( "github.com/docker/model-runner/cmd/cli/desktop" "github.com/docker/model-runner/cmd/cli/pkg/standalone" + "github.com/docker/model-runner/cmd/cli/pkg/types" "github.com/docker/model-runner/pkg/distribution/distribution" "github.com/docker/model-runner/pkg/distribution/oci/reference" "github.com/docker/model-runner/pkg/inference/backends/vllm" @@ -247,6 +249,37 @@ func addRunnerFlags(cmd *cobra.Command, opts runnerFlagOptions) { } } +// syncDockerConfigForRegistry copies the host's Docker config into the running +// container. Only applicable for Moby engine setups; a no-op otherwise. +func syncDockerConfigForRegistry(ctx context.Context, printer standalone.StatusPrinter) { + if modelRunner == nil { + return + } + engineKind := modelRunner.EngineKind() + if engineKind != types.ModelRunnerEngineKindMoby { + return + } + dockerClient, err := desktop.DockerClientForContext(dockerCLI, dockerCLI.CurrentContext()) + if err != nil { + printer.Printf("Warning: failed to create Docker client for credential sync: %v\n", err) + return + } + defer dockerClient.Close() + + containerID, _, _, err := standalone.FindControllerContainer(ctx, dockerClient) + if err != nil { + printer.Printf("Warning: failed to find model runner container for credential sync: %v\n", err) + return + } + if containerID == "" { + return + } + + if err := standalone.SyncDockerConfigToContainer(ctx, dockerClient, containerID, engineKind); err != nil { + printer.Printf("Warning: failed to sync Docker credentials to runner: %v\n", err) + } +} + // newTable creates a new table with Docker CLI-style formatting: // no borders, no column separators, no header line, left-aligned, and 2-space padding. func newTable(w io.Writer) *tablewriter.Table { diff --git a/cmd/cli/pkg/standalone/containers.go b/cmd/cli/pkg/standalone/containers.go index cc6df5eb0..e0ff912f9 100644 --- a/cmd/cli/pkg/standalone/containers.go +++ b/cmd/cli/pkg/standalone/containers.go @@ -28,17 +28,24 @@ import ( // controllerContainerName is the name to use for the controller container. const controllerContainerName = "docker-model-runner" -// copyDockerConfigToContainer copies the Docker config file from the host to the container -// and sets up proper ownership and permissions for the modelrunner user. -// It does nothing for Desktop and Cloud engine kinds. -func copyDockerConfigToContainer(ctx context.Context, dockerClient *client.Client, containerID string, engineKind types.ModelRunnerEngineKind) error { +// SyncDockerConfigToContainer copies the host's ~/.docker/config.json into the +// running container. It is a no-op for Desktop and Cloud engine kinds. +func SyncDockerConfigToContainer(ctx context.Context, dockerClient *client.Client, containerID string, engineKind types.ModelRunnerEngineKind) error { // Do nothing for Desktop and Cloud engine kinds if engineKind == types.ModelRunnerEngineKindDesktop || engineKind == types.ModelRunnerEngineKindCloud || os.Getenv("_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY") == "1" { return nil } - dockerConfigPath := os.ExpandEnv("$HOME/.docker/config.json") + dockerConfigDir := os.Getenv("DOCKER_CONFIG") + if dockerConfigDir == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + dockerConfigDir = filepath.Join(homeDir, ".docker") + } + dockerConfigPath := filepath.Join(dockerConfigDir, "config.json") if s, err := os.Stat(dockerConfigPath); err != nil || s.Mode()&os.ModeType != 0 { return nil } @@ -622,7 +629,7 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, // Copy Docker config file if it exists and we're the container creator. if created && !vllmOnWSL { - if err := copyDockerConfigToContainer(ctx, dockerClient, resp.ID, engineKind); err != nil { + if err := SyncDockerConfigToContainer(ctx, dockerClient, resp.ID, engineKind); err != nil { // Log warning but continue - don't fail container creation printer.Printf("Warning: failed to copy Docker config: %v\n", err) } diff --git a/cmd/cli/pkg/standalone/containers_test.go b/cmd/cli/pkg/standalone/containers_test.go new file mode 100644 index 000000000..31df15e87 --- /dev/null +++ b/cmd/cli/pkg/standalone/containers_test.go @@ -0,0 +1,34 @@ +package standalone + +import ( + "testing" + + "github.com/docker/model-runner/cmd/cli/pkg/types" +) + +// TestSyncDockerConfigToContainer_NoopForDesktopAndCloud verifies that +// SyncDockerConfigToContainer skips Desktop and Cloud engine kinds. +func TestSyncDockerConfigToContainer_NoopForDesktopAndCloud(t *testing.T) { + for _, engineKind := range []types.ModelRunnerEngineKind{ + types.ModelRunnerEngineKindDesktop, + types.ModelRunnerEngineKindCloud, + } { + t.Run(engineKind.String(), func(t *testing.T) { + err := SyncDockerConfigToContainer(t.Context(), nil, "container-id", engineKind) + if err != nil { + t.Fatalf("SyncDockerConfigToContainer(%v) returned unexpected error: %v", engineKind, err) + } + }) + } +} + +// TestSyncDockerConfigToContainer_NoopWhenConfigMissing verifies that +// SyncDockerConfigToContainer skips when the host config file is absent. +func TestSyncDockerConfigToContainer_NoopWhenConfigMissing(t *testing.T) { + t.Setenv("DOCKER_CONFIG", t.TempDir()) + + err := SyncDockerConfigToContainer(t.Context(), nil, "container-id", types.ModelRunnerEngineKindMoby) + if err != nil { + t.Fatalf("SyncDockerConfigToContainer returned unexpected error for missing config: %v", err) + } +}