diff --git a/cmd/cli/commands/install-runner.go b/cmd/cli/commands/install-runner.go index 50c1538e7..0adcada57 100644 --- a/cmd/cli/commands/install-runner.go +++ b/cmd/cli/commands/install-runner.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/docker/docker/api/types/container" "github.com/docker/model-runner/cmd/cli/commands/completion" "github.com/docker/model-runner/cmd/cli/desktop" gpupkg "github.com/docker/model-runner/cmd/cli/pkg/gpu" @@ -18,6 +17,7 @@ import ( "github.com/docker/model-runner/pkg/inference/backends/llamacpp" "github.com/docker/model-runner/pkg/inference/backends/vllm" "github.com/docker/model-runner/pkg/inference/backends/vllmmetal" + "github.com/moby/moby/api/types/container" "github.com/spf13/cobra" ) @@ -55,6 +55,8 @@ type standaloneRunner struct { // hostPort is the port that the runner is listening to on the host. hostPort uint16 // gatewayIP is the gateway IP address that the runner is listening on. + // + // TODO(thaJeztah): consider changing this to a netip.Addr gatewayIP string // gatewayPort is the gateway port that the runner is listening on. gatewayPort uint16 @@ -65,13 +67,15 @@ type standaloneRunner struct { func inspectStandaloneRunner(container container.Summary) *standaloneRunner { result := &standaloneRunner{} for _, port := range container.Ports { - if port.IP == "127.0.0.1" { + if port.IP.IsLoopback() { result.hostPort = port.PublicPort } else { // We don't really have a good way of knowing what the gateway IP // address is, but in the standard standalone configuration we only // bind to two interfaces: 127.0.0.1 and the gateway interface. - result.gatewayIP = port.IP + if port.IP.IsValid() { + result.gatewayIP = port.IP.String() + } result.gatewayPort = port.PublicPort } } diff --git a/cmd/cli/commands/logs.go b/cmd/cli/commands/logs.go index bdc386e56..f3d6bd22d 100644 --- a/cmd/cli/commands/logs.go +++ b/cmd/cli/commands/logs.go @@ -13,12 +13,12 @@ import ( "runtime" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" "github.com/docker/model-runner/cmd/cli/commands/completion" "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/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/client" "github.com/nxadm/tail" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -51,7 +51,7 @@ func newLogsCmd() *cobra.Command { } else if ctrID == "" { return errors.New("unable to identify Model Runner container") } - log, err := dockerClient.ContainerLogs(cmd.Context(), ctrID, container.LogsOptions{ + log, err := dockerClient.ContainerLogs(cmd.Context(), ctrID, client.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Follow: follow, diff --git a/cmd/cli/commands/nim.go b/cmd/cli/commands/nim.go index f90acad1d..69c86542a 100644 --- a/cmd/cli/commands/nim.go +++ b/cmd/cli/commands/nim.go @@ -8,18 +8,18 @@ import ( "fmt" "io" "net/http" + "net/netip" "os" "path/filepath" "strconv" "strings" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" gpupkg "github.com/docker/model-runner/cmd/cli/pkg/gpu" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -137,7 +137,7 @@ func pullNIMImage(ctx context.Context, dockerClient *client.Client, model string } } - pullOptions := image.PullOptions{} + pullOptions := client.ImagePullOptions{} // Set authentication if available if authStr != "" { @@ -186,7 +186,9 @@ func pullNIMImage(ctx context.Context, dockerClient *client.Client, model string defer reader.Close() // Stream pull progress - io.Copy(cmd.OutOrStdout(), reader) + // + // TODO(thaJeztah): format output / progress? + _, _ = io.Copy(cmd.OutOrStdout(), reader) return nil } @@ -195,15 +197,16 @@ func pullNIMImage(ctx context.Context, dockerClient *client.Client, model string func findNIMContainer(ctx context.Context, dockerClient *client.Client, model string) (string, error) { containerName := nimContainerName(model) - containers, err := dockerClient.ContainerList(ctx, container.ListOptions{ + res, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{ All: true, }) if err != nil { return "", fmt.Errorf("failed to list containers: %w", err) } - for _, c := range containers { + for _, c := range res.Items { for _, name := range c.Names { + // TODO(thaJeztah): replace this with a filter, or use "inspect" if strings.TrimPrefix(name, "/") == containerName { return c.ID, nil } @@ -246,17 +249,18 @@ func createNIMContainer(ctx context.Context, dockerClient *client.Client, model } // Container configuration - env := []string{} + var env []string if ngcAPIKey != "" { env = append(env, "NGC_API_KEY="+ngcAPIKey) } - portStr := strconv.Itoa(nimDefaultPort) + hostPort, _ := network.PortFrom(nimDefaultPort, network.TCP) + config := &container.Config{ Image: model, Env: env, - ExposedPorts: nat.PortSet{ - nat.Port(portStr + "/tcp"): struct{}{}, + ExposedPorts: network.PortSet{ + hostPort: struct{}{}, }, } @@ -269,11 +273,11 @@ func createNIMContainer(ctx context.Context, dockerClient *client.Client, model Target: "/opt/nim/.cache", }, }, - PortBindings: nat.PortMap{ - nat.Port(portStr + "/tcp"): []nat.PortBinding{ + PortBindings: network.PortMap{ + hostPort: []network.PortBinding{ { - HostIP: "127.0.0.1", - HostPort: portStr, + HostIP: netip.MustParseAddr("127.0.0.1"), + HostPort: strconv.Itoa(nimDefaultPort), }, }, }, @@ -291,13 +295,19 @@ func createNIMContainer(ctx context.Context, dockerClient *client.Client, model } // Create the container - resp, err := dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, containerName) + resp, err := dockerClient.ContainerCreate(ctx, client.ContainerCreateOptions{ + Config: config, + HostConfig: hostConfig, + NetworkingConfig: nil, + Platform: nil, + Name: containerName, + }) if err != nil { return "", fmt.Errorf("failed to create NIM container: %w", err) } // Start the container - if err := dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + if _, err := dockerClient.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}); err != nil { return "", fmt.Errorf("failed to start NIM container: %w", err) } @@ -315,13 +325,13 @@ func createNIMContainer(ctx context.Context, dockerClient *client.Client, model func waitForNIMReady(ctx context.Context, cmd *cobra.Command) error { cmd.Println("Waiting for NIM to be ready (this may take several minutes)...") - client := &http.Client{ + httpClient := &http.Client{ Timeout: 5 * time.Second, } maxRetries := 120 // 10 minutes with 5 second intervals for i := 0; i < maxRetries; i++ { - resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/v1/models", nimDefaultPort)) + resp, err := httpClient.Get(fmt.Sprintf("http://127.0.0.1:%d/v1/models", nimDefaultPort)) if err == nil { resp.Body.Close() if resp.StatusCode == http.StatusOK { @@ -356,14 +366,14 @@ func runNIMModel(ctx context.Context, dockerClient *client.Client, model string, if containerID != "" { // Container exists, check if it's running - inspect, err := dockerClient.ContainerInspect(ctx, containerID) + inspect, err := dockerClient.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{}) if err != nil { return fmt.Errorf("failed to inspect NIM container: %w", err) } - if !inspect.State.Running { + if !inspect.Container.State.Running { // Container exists but is not running, start it - if err := dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + if _, err := dockerClient.ContainerStart(ctx, containerID, client.ContainerStartOptions{}); err != nil { return fmt.Errorf("failed to start existing NIM container: %w", err) } cmd.Printf("Started existing NIM container %s\n", nimContainerName(model)) @@ -397,7 +407,7 @@ func chatWithNIM(cmd *cobra.Command, model, prompt string) error { // The NIM container runs on localhost:8000 and provides an OpenAI-compatible API // Create a simple HTTP client to talk to the NIM - client := &http.Client{ + httpClient := &http.Client{ Timeout: 300 * time.Second, } @@ -422,7 +432,7 @@ func chatWithNIM(cmd *cobra.Command, model, prompt string) error { req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := httpClient.Do(req) if err != nil { return fmt.Errorf("failed to send request to NIM: %w", err) } diff --git a/cmd/cli/desktop/context.go b/cmd/cli/desktop/context.go index 3b2c6209c..c797086b4 100644 --- a/cmd/cli/desktop/context.go +++ b/cmd/cli/desktop/context.go @@ -15,12 +15,11 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/context/docker" - "github.com/docker/docker/api/types/container" - clientpkg "github.com/docker/docker/client" "github.com/docker/model-runner/cmd/cli/pkg/standalone" "github.com/docker/model-runner/cmd/cli/pkg/types" "github.com/docker/model-runner/pkg/inference" modeltls "github.com/docker/model-runner/pkg/tls" + "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" ) @@ -75,7 +74,7 @@ func isCloudContext(cli *command.DockerCli) bool { } // DockerClientForContext creates a Docker client for the specified context. -func DockerClientForContext(cli *command.DockerCli, name string) (*clientpkg.Client, error) { +func DockerClientForContext(cli *command.DockerCli, name string) (*client.Client, error) { c, err := cli.ContextStore().GetMetadata(name) if err != nil { return nil, fmt.Errorf("unable to load context metadata: %w", err) @@ -85,10 +84,9 @@ func DockerClientForContext(cli *command.DockerCli, name string) (*clientpkg.Cli return nil, fmt.Errorf("unable to determine context endpoint: %w", err) } - opts := []clientpkg.Opt{ - clientpkg.FromEnv, - clientpkg.WithAPIVersionNegotiation(), - clientpkg.WithHost(endpoint.Host), + opts := []client.Opt{ + client.FromEnv, + client.WithHost(endpoint.Host), } helper, err := connhelper.GetConnectionHelper(endpoint.Host) @@ -97,12 +95,12 @@ func DockerClientForContext(cli *command.DockerCli, name string) (*clientpkg.Cli } if helper != nil { opts = append(opts, - clientpkg.WithHost(helper.Host), - clientpkg.WithDialContext(helper.Dialer), + client.WithHost(helper.Host), + client.WithDialContext(helper.Dialer), ) } - return clientpkg.NewClientWithOpts(opts...) + return client.New(opts...) } // ModelRunnerContext encodes the operational context of a Model CLI command and @@ -205,11 +203,14 @@ func wakeUpCloudIfIdle(ctx context.Context, cli *command.DockerCli) error { if err != nil { return fmt.Errorf("failed to create Docker client: %w", err) } + defer dockerClient.Close() // The call is expected to fail with a client error due to nil arguments, but it triggers // Docker Cloud to wake up from idle. Only return unexpected failures (network issues, // server errors) so they're logged as warnings. - _, err = dockerClient.ContainerCreate(ctx, &container.Config{}, nil, nil, nil, "") + _, err = dockerClient.ContainerCreate(ctx, client.ContainerCreateOptions{ + Config: &container.Config{}, + }) if err != nil && !errdefs.IsInvalidArgument(err) { return fmt.Errorf("failed to wake up Docker Cloud: %w", err) } @@ -337,11 +338,21 @@ func DetectContext(ctx context.Context, cli *command.DockerCli, printer standalo // Construct the HTTP client. var httpClient DockerHttpClient if kind == types.ModelRunnerEngineKindDesktop { - dockerClient, err := DockerClientForContext(cli, cli.CurrentContext()) - if err != nil { - return nil, fmt.Errorf("unable to create model runner client: %w", err) + if useTLS { + // For Desktop context, if TLS is enabled, we should either fully support it or fail fast + // Since Desktop context uses Docker client, we need to handle TLS differently + // For now, we'll fail fast to make the behavior clear + return nil, fmt.Errorf("TLS is not supported for Desktop contexts") } - httpClient = dockerClient.HTTPClient() + + // FIXME(thaJeztah): can we get the user-agent in some other way? (or just use a default, specific to DMR)? + // dockerClient, err := DockerClientForContext(cli, cli.CurrentContext()) + // if err != nil { + // return nil, fmt.Errorf("unable to create model runner client: %w", err) + // } + // httpClient = dockerClient.HTTPClient() + // dockerClient.Close() + httpClient = http.DefaultClient } else { httpClient = http.DefaultClient } @@ -353,13 +364,6 @@ func DetectContext(ctx context.Context, cli *command.DockerCli, printer standalo // Construct TLS client if TLS is enabled var tlsClient DockerHttpClient if useTLS { - if kind == types.ModelRunnerEngineKindDesktop { - // For Desktop context, if TLS is enabled, we should either fully support it or fail fast - // Since Desktop context uses Docker client, we need to handle TLS differently - // For now, we'll fail fast to make the behavior clear - return nil, fmt.Errorf("TLS is not supported for Desktop contexts") - } - tlsConfig, err := modeltls.LoadClientTLSConfig(tlsCACert, tlsSkipVerify) if err != nil { return nil, fmt.Errorf("unable to load TLS configuration: %w", err) diff --git a/cmd/cli/desktop/progress.go b/cmd/cli/desktop/progress.go index a2dacfbee..cd0c697f1 100644 --- a/cmd/cli/desktop/progress.go +++ b/cmd/cli/desktop/progress.go @@ -9,10 +9,11 @@ import ( "io" "strings" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-units" "github.com/docker/model-runner/cmd/cli/pkg/standalone" "github.com/docker/model-runner/pkg/distribution/oci" + "github.com/moby/moby/api/types/jsonstream" + "github.com/moby/moby/client/pkg/jsonmessage" ) // DisplayProgress displays progress messages from a model pull/push operation @@ -160,7 +161,7 @@ func writeDockerProgress(w io.Writer, msg *oci.ProgressMessage) error { // Determine status based on progress var status string - var progressDetail *jsonmessage.JSONProgress + var progressDetail *jsonstream.Progress if msg.Layer.Current == 0 { status = "Waiting" @@ -170,7 +171,7 @@ func writeDockerProgress(w io.Writer, msg *oci.ProgressMessage) error { } else { status = "Downloading" } - progressDetail = &jsonmessage.JSONProgress{ + progressDetail = &jsonstream.Progress{ Current: int64(msg.Layer.Current), Total: int64(msg.Layer.Size), } @@ -180,7 +181,7 @@ func writeDockerProgress(w io.Writer, msg *oci.ProgressMessage) error { } else { status = "Pull complete" } - progressDetail = &jsonmessage.JSONProgress{ + progressDetail = &jsonstream.Progress{ Current: int64(msg.Layer.Current), Total: int64(msg.Layer.Size), } @@ -196,7 +197,7 @@ func writeDockerProgress(w io.Writer, msg *oci.ProgressMessage) error { displayID = displayID[:12] } - dockerMsg := jsonmessage.JSONMessage{ + dockerMsg := jsonstream.Message{ ID: displayID, Status: status, Progress: progressDetail, diff --git a/cmd/cli/pkg/gpu/gpu.go b/cmd/cli/pkg/gpu/gpu.go index 01a75d9dc..66baf2aa9 100644 --- a/cmd/cli/pkg/gpu/gpu.go +++ b/cmd/cli/pkg/gpu/gpu.go @@ -4,7 +4,7 @@ import ( "context" "os/exec" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) // GPUSupport encodes the GPU support available on a Docker engine. @@ -27,7 +27,7 @@ const ( func ProbeGPUSupport(ctx context.Context, dockerClient client.SystemAPIClient) (GPUSupport, error) { // Query Docker Engine for its effective configuration. // Docker Info is the source of truth for which runtimes are actually usable. - info, err := dockerClient.Info(ctx) + res, err := dockerClient.Info(ctx, client.InfoOptions{}) if err != nil { // Preserve best-effort behavior: if Docker Info is unavailable (e.g. in // restricted or degraded environments), do not treat this as a hard failure. @@ -48,7 +48,7 @@ func ProbeGPUSupport(ctx context.Context, dockerClient client.SystemAPIClient) ( } for _, r := range supportedRuntimes { - if _, ok := info.Runtimes[r.name]; ok { + if _, ok := res.Info.Runtimes[r.name]; ok { return r.support, nil } } @@ -66,40 +66,29 @@ func ProbeGPUSupport(ctx context.Context, dockerClient client.SystemAPIClient) ( // HasNVIDIARuntime determines whether there is an nvidia runtime available func HasNVIDIARuntime(ctx context.Context, dockerClient client.SystemAPIClient) (bool, error) { - info, err := dockerClient.Info(ctx) - if err != nil { - return false, err - } - _, hasNvidia := info.Runtimes["nvidia"] - return hasNvidia, nil + return hasRuntime(ctx, dockerClient, "nvidia") } // HasROCmRuntime determines whether there is a ROCm runtime available func HasROCmRuntime(ctx context.Context, dockerClient client.SystemAPIClient) (bool, error) { - info, err := dockerClient.Info(ctx) - if err != nil { - return false, err - } - _, hasROCm := info.Runtimes["rocm"] - return hasROCm, nil + return hasRuntime(ctx, dockerClient, "rocm") } // HasMTHREADSRuntime determines whether there is a mthreads runtime available func HasMTHREADSRuntime(ctx context.Context, dockerClient client.SystemAPIClient) (bool, error) { - info, err := dockerClient.Info(ctx) - if err != nil { - return false, err - } - _, hasMTHREADS := info.Runtimes["mthreads"] - return hasMTHREADS, nil + return hasRuntime(ctx, dockerClient, "mthreads") } // HasCANNRuntime determines whether there is a Ascend CANN runtime available func HasCANNRuntime(ctx context.Context, dockerClient client.SystemAPIClient) (bool, error) { - info, err := dockerClient.Info(ctx) + return hasRuntime(ctx, dockerClient, "cann") +} + +func hasRuntime(ctx context.Context, dockerClient client.SystemAPIClient, runtimeName string) (bool, error) { + res, err := dockerClient.Info(ctx, client.InfoOptions{}) if err != nil { return false, err } - _, hasCANN := info.Runtimes["cann"] - return hasCANN, nil + _, ok := res.Info.Runtimes[runtimeName] + return ok, nil } diff --git a/cmd/cli/pkg/standalone/containers.go b/cmd/cli/pkg/standalone/containers.go index 4e7579b83..c72e6a437 100644 --- a/cmd/cli/pkg/standalone/containers.go +++ b/cmd/cli/pkg/standalone/containers.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "net/netip" "os" "os/exec" "path/filepath" @@ -16,14 +17,12 @@ import ( "time" "github.com/containerd/errdefs" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" gpupkg "github.com/docker/model-runner/cmd/cli/pkg/gpu" "github.com/docker/model-runner/cmd/cli/pkg/types" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" ) // controllerContainerName is the name to use for the controller container. @@ -51,6 +50,8 @@ func copyDockerConfigToContainer(ctx context.Context, dockerClient *client.Clien var buf bytes.Buffer tw := tar.NewWriter(&buf) + defer tw.Close() + header := &tar.Header{ Name: ".docker/config.json", Mode: 0600, @@ -73,8 +74,10 @@ func copyDockerConfigToContainer(ctx context.Context, dockerClient *client.Clien } // Copy directly into the .docker directory - err = dockerClient.CopyToContainer(ctx, containerID, "/home/modelrunner", &buf, container.CopyToContainerOptions{ - CopyUIDGID: true, + _, err = dockerClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{ + DestinationPath: "/home/modelrunner", + Content: &buf, + CopyUIDGID: true, }) if err != nil { return fmt.Errorf("failed to copy config file to container: %w", err) @@ -90,17 +93,18 @@ func copyDockerConfigToContainer(ctx context.Context, dockerClient *client.Clien } func execInContainer(ctx context.Context, dockerClient *client.Client, containerID, cmd string, asRoot bool) error { - execConfig := container.ExecOptions{ - Cmd: []string{"sh", "-c", cmd}, - } + var user string if asRoot { - execConfig.User = "root" + user = "root" } - execResp, err := dockerClient.ContainerExecCreate(ctx, containerID, execConfig) + execResp, err := dockerClient.ExecCreate(ctx, containerID, client.ExecCreateOptions{ + Cmd: []string{"sh", "-c", cmd}, + User: user, + }) if err != nil { return fmt.Errorf("failed to create exec for command '%s': %w", cmd, err) } - if err := dockerClient.ContainerExecStart(ctx, execResp.ID, container.ExecStartOptions{}); err != nil { + if _, err := dockerClient.ExecStart(ctx, execResp.ID, client.ExecStartOptions{}); err != nil { return fmt.Errorf("failed to start exec for command '%s': %w", cmd, err) } @@ -110,7 +114,7 @@ func execInContainer(ctx context.Context, dockerClient *client.Client, container // Poll until the command finishes or timeout occurs for { - inspectResp, err := dockerClient.ContainerExecInspect(ctx, execResp.ID) + inspectResp, err := dockerClient.ExecInspect(ctx, execResp.ID, client.ExecInspectOptions{}) if err != nil { return fmt.Errorf("failed to inspect exec for command '%s': %w", cmd, err) } @@ -143,38 +147,37 @@ func FindControllerContainer(ctx context.Context, dockerClient client.ContainerA } // Identify all controller containers. - containers, err := dockerClient.ContainerList(ctx, container.ListOptions{ - Filters: filters.NewArgs( - // Don't include a value on this first label selector; Docker Cloud - // middleware only shows these containers if no value is queried. - filters.Arg("label", labelDesktopService), - filters.Arg("label", labelRole+"="+roleController), - ), + res, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{ + // Don't include a value on this first label selector; Docker Cloud + // middleware only shows these containers if no value is queried. + Filters: make(client.Filters).Add("label", labelDesktopService, labelRole+"="+roleController), }) if err != nil { return "", "", container.Summary{}, fmt.Errorf("unable to identify model runner containers: %w", err) } - if len(containers) == 0 { + if len(res.Items) == 0 { return "", "", container.Summary{}, nil } + ctr := res.Items[0] + var containerName string - if len(containers[0].Names) > 0 { - containerName = strings.TrimPrefix(containers[0].Names[0], "/") + if len(ctr.Names) > 0 { + containerName = strings.TrimPrefix(ctr.Names[0], "/") } - return containers[0].ID, containerName, containers[0], nil + return ctr.ID, containerName, ctr, nil } // determineBridgeGatewayIP attempts to identify the engine's host gateway IP // address on the bridge network. It may return an empty IP address even with a // nil error if no IP could be identified. func determineBridgeGatewayIP(ctx context.Context, dockerClient client.NetworkAPIClient) (string, error) { - bridge, err := dockerClient.NetworkInspect(ctx, "bridge", network.InspectOptions{}) + res, err := dockerClient.NetworkInspect(ctx, "bridge", client.NetworkInspectOptions{}) if err != nil { return "", err } - for _, config := range bridge.IPAM.Config { - if config.Gateway != "" { - return config.Gateway, nil + for _, config := range res.Network.IPAM.Config { + if config.Gateway.IsValid() { + return config.Gateway.String(), nil } } return "", nil @@ -184,7 +187,7 @@ func determineBridgeGatewayIP(ctx context.Context, dockerClient client.NetworkAP // concurrently, taking advantage of the fact that ContainerStart is idempotent. func ensureContainerStarted(ctx context.Context, dockerClient client.ContainerAPIClient, containerID string) error { for i := 10; i > 0; i-- { - err := dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}) + _, err := dockerClient.ContainerStart(ctx, containerID, client.ContainerStartOptions{}) if err == nil { return nil } @@ -222,12 +225,12 @@ func ensureContainerStarted(ctx context.Context, dockerClient client.ContainerAP // isRootless detects if Docker is running in rootless mode. func isRootless(ctx context.Context, dockerClient *client.Client) bool { - info, err := dockerClient.Info(ctx) + res, err := dockerClient.Info(ctx, client.InfoOptions{}) if err != nil { // If we can't get Docker info, assume it's not rootless to preserve old behavior. return false } - for _, opt := range info.SecurityOptions { + for _, opt := range res.Info.SecurityOptions { if strings.Contains(opt, "rootless") { return true } @@ -304,8 +307,18 @@ func isPortBindingError(err error) bool { func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, port uint16, host string, environment string, doNotTrack bool, gpu gpupkg.GPUSupport, backend string, modelStorageVolume string, printer StatusPrinter, engineKind types.ModelRunnerEngineKind, debug bool, vllmOnWSL bool, proxyCert string, tlsOpts TLSOptions) error { imageName := controllerImageName(gpu, backend) + var hostIP netip.Addr + if host != "" { + p, err := netip.ParseAddr(host) + if err != nil { + return fmt.Errorf("invalid host: must be a valid IP-address: %w", err) + } + hostIP = p + } + // Set up the container configuration. portStr := strconv.Itoa(int(port)) + expPort, _ := network.PortFrom(port, network.TCP) env := []string{ "MODEL_RUNNER_PORT=" + portStr, "MODEL_RUNNER_ENVIRONMENT=" + environment, @@ -331,6 +344,7 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, tlsPort = DefaultTLSPortMoby } } + expTLSPort, _ := network.PortFrom(tlsPort, network.TCP) // Add TLS environment variables if TLS is enabled if tlsOpts.Enabled { @@ -356,11 +370,11 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, // If no cert paths, auto-cert will be used inside the container } - exposedPorts := nat.PortSet{ - nat.Port(portStr + "/tcp"): struct{}{}, + exposedPorts := network.PortSet{ + expPort: struct{}{}, } if tlsOpts.Enabled { - exposedPorts[nat.Port(strconv.Itoa(int(tlsPort))+"/tcp")] = struct{}{} + exposedPorts[expTLSPort] = struct{}{} } config := &container.Config{ @@ -443,14 +457,24 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, } // Helper function to create port bindings with optional bridge gateway IP - createPortBindings := func(port string) []nat.PortBinding { - portBindings := []nat.PortBinding{{HostIP: host, HostPort: port}} + createPortBindings := func(port string) []network.PortBinding { + portBindings := []network.PortBinding{{ + HostIP: hostIP, + HostPort: port, + }} if os.Getenv("_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY") != "1" { // Don't bind the bridge gateway IP if we're treating Docker Desktop as Moby. // Only add bridge gateway IP binding if host is 127.0.0.1 and not in rootless mode if host == "127.0.0.1" && !isRootless(ctx, dockerClient) && !vllmOnWSL { if bridgeGatewayIP, err := determineBridgeGatewayIP(ctx, dockerClient); err == nil && bridgeGatewayIP != "" { - portBindings = append(portBindings, nat.PortBinding{HostIP: bridgeGatewayIP, HostPort: port}) + var gwIP netip.Addr + if p, err := netip.ParseAddr(bridgeGatewayIP); err == nil { + gwIP = p + } + portBindings = append(portBindings, network.PortBinding{ + HostIP: gwIP, + HostPort: port, + }) } } } @@ -458,14 +482,14 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, } // Create port bindings for the main port - hostConfig.PortBindings = nat.PortMap{ - nat.Port(portStr + "/tcp"): createPortBindings(portStr), + hostConfig.PortBindings = network.PortMap{ + expPort: createPortBindings(portStr), } // Add TLS port bindings if TLS is enabled if tlsOpts.Enabled { tlsPortStr := strconv.Itoa(int(tlsPort)) - hostConfig.PortBindings[nat.Port(tlsPortStr+"/tcp")] = createPortBindings(tlsPortStr) + hostConfig.PortBindings[expTLSPort] = createPortBindings(tlsPortStr) } switch gpu { case gpupkg.GPUSupportNone: @@ -553,7 +577,13 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, // pass silently and simply work in conjunction with any concurrent // installers to start the container. // TODO: Remove strings.Contains check once we ensure it's not necessary. - resp, err := dockerClient.ContainerCreate(ctx, config, hostConfig, nil, nil, controllerContainerName) + resp, err := dockerClient.ContainerCreate(ctx, client.ContainerCreateOptions{ + Config: config, + HostConfig: hostConfig, + NetworkingConfig: nil, + Platform: nil, + Name: controllerContainerName, + }) if err != nil && !errdefs.IsConflict(err) && !strings.Contains(err.Error(), "is already in use by container") { return fmt.Errorf("failed to create container %s: %w", controllerContainerName, err) } @@ -563,7 +593,7 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, printer.Printf("Starting model runner container %s...\n", controllerContainerName) if err := ensureContainerStarted(ctx, dockerClient, controllerContainerName); err != nil { if created { - _ = dockerClient.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) + _, _ = dockerClient.ContainerRemove(ctx, resp.ID, client.ContainerRemoveOptions{Force: true}) } if isPortBindingError(err) { return fmt.Errorf("failed to start container %s: %w\n\nThe port may already be in use by Docker Desktop's Model Runner.\nTry running: docker desktop disable model-runner", controllerContainerName, err) @@ -586,7 +616,7 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, printer.Printf("Warning: failed to update CA certificates: %v\n", err) } else { printer.Printf("Restarting container to apply CA certificate...\n") - if err := dockerClient.ContainerRestart(ctx, resp.ID, container.StopOptions{}); err != nil { + if _, err := dockerClient.ContainerRestart(ctx, resp.ID, client.ContainerRestartOptions{}); err != nil { printer.Printf("Warning: failed to restart container after adding CA certificate: %v\n", err) } } @@ -599,21 +629,18 @@ func CreateControllerContainer(ctx context.Context, dockerClient *client.Client, // containers. func PruneControllerContainers(ctx context.Context, dockerClient client.ContainerAPIClient, skipRunning bool, printer StatusPrinter) error { // Identify all controller containers. - containers, err := dockerClient.ContainerList(ctx, container.ListOptions{ + res, err := dockerClient.ContainerList(ctx, client.ContainerListOptions{ All: true, - Filters: filters.NewArgs( - // Don't include a value on this first label selector; Docker Cloud - // middleware only shows these containers if no value is queried. - filters.Arg("label", labelDesktopService), - filters.Arg("label", labelRole+"="+roleController), - ), + // Don't include a value on this first label selector; Docker Cloud + // middleware only shows these containers if no value is queried. + Filters: make(client.Filters).Add("label", labelDesktopService, labelRole+"="+roleController), }) if err != nil { return fmt.Errorf("unable to identify model runner containers: %w", err) } // Remove all controller containers. - for _, ctr := range containers { + for _, ctr := range res.Items { if skipRunning && ctr.State == container.StateRunning { continue } @@ -622,7 +649,7 @@ func PruneControllerContainers(ctx context.Context, dockerClient client.Containe } else { printer.Printf("Removing container %s...\n", ctr.ID[:12]) } - err := dockerClient.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{Force: true}) + _, err := dockerClient.ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{Force: true}) if err != nil { return fmt.Errorf("failed to remove container %s: %w", ctr.Names[0], err) } diff --git a/cmd/cli/pkg/standalone/images.go b/cmd/cli/pkg/standalone/images.go index 1808012e1..9a3adffbf 100644 --- a/cmd/cli/pkg/standalone/images.go +++ b/cmd/cli/pkg/standalone/images.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" gpupkg "github.com/docker/model-runner/cmd/cli/pkg/gpu" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/jsonmessage" ) // EnsureControllerImage ensures that the controller container image is pulled. @@ -15,7 +14,7 @@ func EnsureControllerImage(ctx context.Context, dockerClient client.ImageAPIClie imageName := controllerImageName(gpu, backend) // Perform the pull. - out, err := dockerClient.ImagePull(ctx, imageName, image.PullOptions{}) + out, err := dockerClient.ImagePull(ctx, imageName, client.ImagePullOptions{}) if err != nil { return fmt.Errorf("failed to pull image %s: %w", imageName, err) } @@ -35,13 +34,13 @@ func EnsureControllerImage(ctx context.Context, dockerClient client.ImageAPIClie func PruneControllerImages(ctx context.Context, dockerClient client.ImageAPIClient, printer StatusPrinter) error { // Remove the standard image, if present. imageNameCPU := fmtControllerImageName(ControllerImage, controllerImageVersion(), "") - if _, err := dockerClient.ImageRemove(ctx, imageNameCPU, image.RemoveOptions{}); err == nil { + if _, err := dockerClient.ImageRemove(ctx, imageNameCPU, client.ImageRemoveOptions{}); err == nil { printer.Println("Removed image", imageNameCPU) } // Remove the CUDA GPU image, if present. imageNameCUDA := fmtControllerImageName(ControllerImage, controllerImageVersion(), "cuda") - if _, err := dockerClient.ImageRemove(ctx, imageNameCUDA, image.RemoveOptions{}); err == nil { + if _, err := dockerClient.ImageRemove(ctx, imageNameCUDA, client.ImageRemoveOptions{}); err == nil { printer.Println("Removed image", imageNameCUDA) } return nil diff --git a/cmd/cli/pkg/standalone/volumes.go b/cmd/cli/pkg/standalone/volumes.go index ce5ddc06e..33a1b1106 100644 --- a/cmd/cli/pkg/standalone/volumes.go +++ b/cmd/cli/pkg/standalone/volumes.go @@ -4,9 +4,7 @@ import ( "context" "fmt" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) // modelStorageVolumeName is the name to use for the model storage volume. @@ -17,10 +15,8 @@ const modelStorageVolumeName = "docker-model-runner-models" // occurred. func EnsureModelStorageVolume(ctx context.Context, dockerClient client.VolumeAPIClient, printer StatusPrinter) (string, error) { // Try to identify the storage volume. - volumes, err := dockerClient.VolumeList(ctx, volume.ListOptions{ - Filters: filters.NewArgs( - filters.Arg("label", labelRole+"="+roleModelStorage), - ), + res, err := dockerClient.VolumeList(ctx, client.VolumeListOptions{ + Filters: make(client.Filters).Add("label", labelRole+"="+roleModelStorage), }) if err != nil { return "", fmt.Errorf("unable to list volumes: %w", err) @@ -28,13 +24,13 @@ func EnsureModelStorageVolume(ctx context.Context, dockerClient client.VolumeAPI // If any volumes with the correct role exist (ideally there should only be // one), then pick the first one. - if len(volumes.Volumes) > 0 { - return volumes.Volumes[0].Name, nil + if len(res.Items) > 0 { + return res.Items[0].Name, nil } // Create the volume. printer.Printf("Creating model storage volume %s...\n", modelStorageVolumeName) - volume, err := dockerClient.VolumeCreate(ctx, volume.CreateOptions{ + resp, err := dockerClient.VolumeCreate(ctx, client.VolumeCreateOptions{ Name: modelStorageVolumeName, Labels: map[string]string{ labelDesktopService: serviceModelRunner, @@ -44,23 +40,22 @@ func EnsureModelStorageVolume(ctx context.Context, dockerClient client.VolumeAPI if err != nil { return "", fmt.Errorf("unable to create volume: %w", err) } - return volume.Name, nil + return resp.Volume.Name, nil } // PruneModelStorageVolumes removes any unused model storage volume(s). func PruneModelStorageVolumes(ctx context.Context, dockerClient client.VolumeAPIClient, printer StatusPrinter) error { - pruned, err := dockerClient.VolumesPrune(ctx, filters.NewArgs( - filters.Arg("all", "true"), - filters.Arg("label", labelRole+"="+roleModelStorage), - )) + pruned, err := dockerClient.VolumePrune(ctx, client.VolumePruneOptions{ + Filters: make(client.Filters).Add("all", "true").Add("label", labelRole+"="+roleModelStorage), + }) if err != nil { return err } - for _, volume := range pruned.VolumesDeleted { + for _, volume := range pruned.Report.VolumesDeleted { printer.Println("Removed volume", volume) } - if pruned.SpaceReclaimed > 0 { - printer.Printf("Reclaimed %d bytes\n", pruned.SpaceReclaimed) + if pruned.Report.SpaceReclaimed > 0 { + printer.Printf("Reclaimed %d bytes\n", pruned.Report.SpaceReclaimed) } return nil } diff --git a/go.mod b/go.mod index 94d4cc4f9..70ffcbddb 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/docker/cli v29.2.1+incompatible github.com/docker/cli-docs-tool v0.11.0 - github.com/docker/docker v28.5.2+incompatible github.com/docker/docker-credential-helpers v0.9.5 - github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/emirpasic/gods/v2 v2.0.0-alpha github.com/fatih/color v1.18.0 @@ -21,6 +19,7 @@ require ( github.com/kolesnikovae/go-winjob v1.0.0 github.com/mattn/go-runewidth v0.0.20 github.com/mattn/go-shellwords v1.0.12 + github.com/moby/moby/api v1.53.0 github.com/moby/moby/client v0.2.2 github.com/moby/term v0.5.2 github.com/muesli/termenv v0.16.0 @@ -68,9 +67,11 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -87,7 +88,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jaypipes/pcidb v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect @@ -97,7 +98,6 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/moby/api v1.53.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect @@ -111,7 +111,7 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index f5157af90..9b69b6ebe 100644 --- a/go.sum +++ b/go.sum @@ -76,9 +76,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= @@ -157,8 +156,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -234,9 +233,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=