Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 72 additions & 5 deletions cmd/cli/commands/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@ package commands

import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"time"

"github.com/docker/cli/cli/command"
"github.com/docker/model-runner/cmd/cli/commands/formatter"
"github.com/docker/model-runner/cmd/cli/desktop"
"github.com/docker/model-runner/cmd/cli/pkg/modelctx"
"github.com/docker/model-runner/cmd/cli/pkg/standalone"
"github.com/docker/model-runner/cmd/cli/pkg/types"
"github.com/spf13/cobra"
)

// defaultContextDetectTimeout is the maximum time allowed for detecting the
// engine kind when building the synthetic "default" context row.
const defaultContextDetectTimeout = 2 * time.Second

// newContextCmd returns the "docker model context" parent command. Its
// subcommands manage named Model Runner contexts stored on disk, so they do
// not require a running backend and override PersistentPreRunE accordingly.
Expand Down Expand Up @@ -66,6 +75,52 @@ func dockerConfigDir() (string, error) {
return filepath.Join(home, ".docker"), nil
}

// resolveKindHost maps an engine kind and TLS preference to the host string
// shown in "context ls" / "context inspect". Desktop returns the
// human-readable engine name; other kinds return a localhost URL.
func resolveKindHost(kind types.ModelRunnerEngineKind, useTLS bool) string {
switch kind {
case types.ModelRunnerEngineKindDesktop:
return kind.String()
case types.ModelRunnerEngineKindCloud:
if useTLS {
return "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortCloud)
}
return "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud)
case types.ModelRunnerEngineKindMoby, types.ModelRunnerEngineKindMobyManual:
if useTLS {
return "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortMoby)
}
return "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby)
default:
return "(auto-detect)"
}
}

// resolveDefaultContext attempts to detect the Docker engine kind for the
// synthetic "default" context row. When the CLI is available it probes the
// Docker daemon (with a short timeout) and returns a descriptive host and
// description derived from the detected engine kind. If detection fails or
// the CLI is unavailable (nil) it falls back to generic strings.
func resolveDefaultContext(ctx context.Context) (host, description string) {
const fallbackHost = "(auto-detect)"
const fallbackDescription = "Auto-detected Docker context"

// dockerCLI is nil during tests and when the CLI is not yet initialised.
if dockerCLI == nil {
return fallbackHost, fallbackDescription
}

detectCtx, cancel := context.WithTimeout(ctx, defaultContextDetectTimeout)
defer cancel()

kind := desktop.DetectEngineKind(detectCtx, dockerCLI)
// Mirror DetectContext: MODEL_RUNNER_TLS must be explicitly set to "true".
tlsVal, tlsSet := os.LookupEnv("MODEL_RUNNER_TLS")
useTLS := tlsSet && tlsVal == "true"
return resolveKindHost(kind, useTLS), "Model Runner on " + kind.String()
}

// newContextCreateCmd returns the "context create" command.
func newContextCreateCmd() *cobra.Command {
var (
Expand Down Expand Up @@ -223,12 +278,16 @@ func newContextLsCmd() *cobra.Command {
)
}

// Resolve the host and description for the synthetic "default"
// row by detecting the engine kind (Desktop, Moby, Cloud).
defaultHost, defaultDescription := resolveDefaultContext(cmd.Context())

// Build rows: synthetic "default" first, then named contexts sorted.
rows := []contextListRow{
{
name: modelctx.DefaultContextName,
host: "(auto-detect)",
description: "Auto-detected Docker context",
host: defaultHost,
description: defaultDescription,
active: activeName == modelctx.DefaultContextName,
},
}
Expand Down Expand Up @@ -325,15 +384,23 @@ func newContextInspectCmd() *cobra.Command {
return fmt.Errorf("unable to open context store: %w", err)
}

// Resolve the default context info once (lazily) so that
// repeated "default" args do not trigger multiple probes.
var defaultHost, defaultDescription string
defaultResolved := false

results := make([]namedContextInspect, 0, len(args))
for _, name := range args {
if name == modelctx.DefaultContextName {
// Return a synthetic entry for "default".
if !defaultResolved {
defaultHost, defaultDescription = resolveDefaultContext(cmd.Context())
defaultResolved = true
}
results = append(results, namedContextInspect{
Name: modelctx.DefaultContextName,
ContextConfig: modelctx.ContextConfig{
Host: "(auto-detect)",
Description: "Auto-detected Docker context",
Host: defaultHost,
Description: defaultDescription,
},
})
continue
Expand Down
44 changes: 44 additions & 0 deletions cmd/cli/commands/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package commands
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"testing"

"github.com/docker/model-runner/cmd/cli/pkg/modelctx"
"github.com/docker/model-runner/cmd/cli/pkg/standalone"
"github.com/docker/model-runner/cmd/cli/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -395,3 +398,44 @@ func TestContextInspect_notFound(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}

// TestResolveKindHost verifies that every engine kind maps to the expected
// host string, including TLS variants and the unknown-kind fallback.
func TestResolveKindHost(t *testing.T) {
mobyPlain := "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby)
mobyTLS := "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortMoby)
cloudPlain := "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud)
cloudTLS := "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortCloud)

tests := []struct {
name string
kind types.ModelRunnerEngineKind
useTLS bool
want string
}{
{"desktop_plain", types.ModelRunnerEngineKindDesktop, false, "Docker Desktop"},
{"desktop_tls_ignored", types.ModelRunnerEngineKindDesktop, true, "Docker Desktop"},
{"cloud_plain", types.ModelRunnerEngineKindCloud, false, cloudPlain},
{"cloud_tls", types.ModelRunnerEngineKindCloud, true, cloudTLS},
{"moby_plain", types.ModelRunnerEngineKindMoby, false, mobyPlain},
{"moby_tls", types.ModelRunnerEngineKindMoby, true, mobyTLS},
{"moby_manual_plain", types.ModelRunnerEngineKindMobyManual, false, mobyPlain},
{"moby_manual_tls", types.ModelRunnerEngineKindMobyManual, true, mobyTLS},
{"unknown_fallback", types.ModelRunnerEngineKind(99), false, "(auto-detect)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, resolveKindHost(tt.kind, tt.useTLS))
})
}
}

// TestResolveDefaultContext_nilCLI documents the nil-CLI contract: when
// dockerCLI is not set, the fallback strings are returned immediately without
// any network probe.
func TestResolveDefaultContext_nilCLI(t *testing.T) {
dockerCLI = nil
host, desc := resolveDefaultContext(t.Context())
assert.Equal(t, "(auto-detect)", host)
assert.Equal(t, "Auto-detected Docker context", desc)
}
26 changes: 26 additions & 0 deletions cmd/cli/desktop/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,32 @@ func namedContextStore(cli *command.DockerCli) (*modelctx.Store, error) {
return modelctx.New(configDir)
}

// DetectEngineKind determines the Docker engine kind associated with the
// current CLI context without performing any side effects (such as waking
// up Docker Cloud). It is intended for informational commands like
// "context ls" that need to display the resolved engine kind without
// triggering backend initialisation.
func DetectEngineKind(ctx context.Context, cli *command.DockerCli) types.ModelRunnerEngineKind {
if isDesktopContext(ctx, cli) {
// On WSL2, a Moby-based controller container may be running
// alongside Docker Desktop. Mirror the logic in DetectContext
// so that "context ls" reports the same engine kind.
if IsDesktopWSLContext(ctx, cli) {
if dockerClient, err := DockerClientForContext(cli, cli.CurrentContext()); err == nil {
defer dockerClient.Close()
if containerID, _, _, findErr := standalone.FindControllerContainer(ctx, dockerClient); findErr == nil && containerID != "" {
return types.ModelRunnerEngineKindMoby
}
}
}
return types.ModelRunnerEngineKindDesktop
}
if isCloudContext(cli) {
return types.ModelRunnerEngineKindCloud
}
return types.ModelRunnerEngineKindMoby
}
Comment thread
ilopezluna marked this conversation as resolved.

// DetectContext determines the current Docker Model Runner context.
func DetectContext(ctx context.Context, cli *command.DockerCli, printer standalone.StatusPrinter) (*ModelRunnerContext, error) {
// Check for an explicit endpoint setting.
Expand Down
Loading