From 84b73705493ed65f692699347c44815c40a46c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 2 Apr 2026 12:54:06 +0200 Subject: [PATCH 1/6] feat: implement engine kind detection for default context in context ls --- cmd/cli/commands/context.go | 56 +++++++++++++++++++++++++++++++++---- cmd/cli/desktop/context.go | 15 ++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/cmd/cli/commands/context.go b/cmd/cli/commands/context.go index 4a1d1489c..86f6cea1f 100644 --- a/cmd/cli/commands/context.go +++ b/cmd/cli/commands/context.go @@ -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. @@ -66,6 +75,37 @@ func dockerConfigDir() (string, error) { return filepath.Join(home, ".docker"), nil } +// 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 it falls back to generic strings. +func resolveDefaultContext(ctx context.Context) (host, description string) { + const fallbackHost = "(auto-detect)" + const fallbackDescription = "Auto-detected Docker context" + + if dockerCLI == nil { + return fallbackHost, fallbackDescription + } + + detectCtx, cancel := context.WithTimeout(ctx, defaultContextDetectTimeout) + defer cancel() + + kind := desktop.DetectEngineKind(detectCtx, dockerCLI) + description = "Model Runner on " + kind.String() + + switch kind { + case types.ModelRunnerEngineKindDesktop: + host = kind.String() + case types.ModelRunnerEngineKindCloud: + host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud) + case types.ModelRunnerEngineKindMoby, types.ModelRunnerEngineKindMobyManual: + host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby) + } + + return host, description +} + // newContextCreateCmd returns the "context create" command. func newContextCreateCmd() *cobra.Command { var ( @@ -223,12 +263,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, }, } @@ -328,12 +372,14 @@ func newContextInspectCmd() *cobra.Command { results := make([]namedContextInspect, 0, len(args)) for _, name := range args { if name == modelctx.DefaultContextName { - // Return a synthetic entry for "default". + // Return a synthetic entry for "default", + // resolved from the detected engine kind. + defaultHost, defaultDescription := resolveDefaultContext(cmd.Context()) results = append(results, namedContextInspect{ Name: modelctx.DefaultContextName, ContextConfig: modelctx.ContextConfig{ - Host: "(auto-detect)", - Description: "Auto-detected Docker context", + Host: defaultHost, + Description: defaultDescription, }, }) continue diff --git a/cmd/cli/desktop/context.go b/cmd/cli/desktop/context.go index 7e6546b2c..8f3659dc1 100644 --- a/cmd/cli/desktop/context.go +++ b/cmd/cli/desktop/context.go @@ -243,6 +243,21 @@ 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) { + return types.ModelRunnerEngineKindDesktop + } + if isCloudContext(cli) { + return types.ModelRunnerEngineKindCloud + } + return types.ModelRunnerEngineKindMoby +} + // 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. From c6ef7c1bca51d854c8e9cbc5197fa37504f98287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 2 Apr 2026 13:12:52 +0200 Subject: [PATCH 2/6] fix: address review feedback for engine kind detection - Handle WSL2 edge case in DetectEngineKind where a Moby controller container may be running alongside Docker Desktop - Respect MODEL_RUNNER_TLS env var in resolveDefaultContext to show correct scheme (https) and TLS ports (12444/12445) when TLS is enabled - Resolve default context info lazily once in inspect command to avoid repeated network probes when 'default' appears multiple times in args --- cmd/cli/commands/context.go | 28 +++++++++++++++++++++++----- cmd/cli/desktop/context.go | 11 +++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cmd/cli/commands/context.go b/cmd/cli/commands/context.go index 86f6cea1f..fa45d1169 100644 --- a/cmd/cli/commands/context.go +++ b/cmd/cli/commands/context.go @@ -94,13 +94,25 @@ func resolveDefaultContext(ctx context.Context) (host, description string) { kind := desktop.DetectEngineKind(detectCtx, dockerCLI) description = "Model Runner on " + kind.String() + // Check whether TLS is enabled so the displayed scheme and port match + // what DetectContext would actually use at runtime. + useTLS := os.Getenv("MODEL_RUNNER_TLS") == "true" + switch kind { case types.ModelRunnerEngineKindDesktop: host = kind.String() case types.ModelRunnerEngineKindCloud: - host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud) + if useTLS { + host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortCloud) + } else { + host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud) + } case types.ModelRunnerEngineKindMoby, types.ModelRunnerEngineKindMobyManual: - host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby) + if useTLS { + host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortMoby) + } else { + host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby) + } } return host, description @@ -369,12 +381,18 @@ 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", - // resolved from the detected engine kind. - defaultHost, defaultDescription := resolveDefaultContext(cmd.Context()) + if !defaultResolved { + defaultHost, defaultDescription = resolveDefaultContext(cmd.Context()) + defaultResolved = true + } results = append(results, namedContextInspect{ Name: modelctx.DefaultContextName, ContextConfig: modelctx.ContextConfig{ diff --git a/cmd/cli/desktop/context.go b/cmd/cli/desktop/context.go index 8f3659dc1..17a059ffe 100644 --- a/cmd/cli/desktop/context.go +++ b/cmd/cli/desktop/context.go @@ -250,6 +250,17 @@ func namedContextStore(cli *command.DockerCli) (*modelctx.Store, error) { // 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) { From d3e11a3c465a35d9fdc3b9c9318402984ef241cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 2 Apr 2026 16:45:48 +0200 Subject: [PATCH 3/6] fix: use envconfig for TLS checks in engine kind detection --- cmd/cli/commands/context.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cmd/cli/commands/context.go b/cmd/cli/commands/context.go index fa45d1169..9a1843cc9 100644 --- a/cmd/cli/commands/context.go +++ b/cmd/cli/commands/context.go @@ -17,6 +17,7 @@ import ( "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/docker/model-runner/pkg/envconfig" "github.com/spf13/cobra" ) @@ -94,21 +95,17 @@ func resolveDefaultContext(ctx context.Context) (host, description string) { kind := desktop.DetectEngineKind(detectCtx, dockerCLI) description = "Model Runner on " + kind.String() - // Check whether TLS is enabled so the displayed scheme and port match - // what DetectContext would actually use at runtime. - useTLS := os.Getenv("MODEL_RUNNER_TLS") == "true" - switch kind { case types.ModelRunnerEngineKindDesktop: host = kind.String() case types.ModelRunnerEngineKindCloud: - if useTLS { + if envconfig.TLSEnabled() { host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortCloud) } else { host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud) } case types.ModelRunnerEngineKindMoby, types.ModelRunnerEngineKindMobyManual: - if useTLS { + if envconfig.TLSEnabled() { host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortMoby) } else { host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby) From 90f49cb3c5f512699f39b3d03ef884292c270164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 2 Jul 2026 11:57:35 +0200 Subject: [PATCH 4/6] fix: address remaining review feedback for engine kind detection - Extract resolveKindHost helper with exhaustive switch and default fallback, fixing the missing-default-case issue raised by reviewers - Fix TLS env var: use MODEL_RUNNER_TLS (client-side, matching DetectContext) instead of MODEL_RUNNER_TLS_ENABLED (server-side) - Remove unused envconfig import - Add TestResolveKindHost covering all engine kinds with/without TLS and the unknown-kind fallback, plus TestResolveDefaultContext_nilCLI to document the nil-CLI contract Co-Authored-By: Claude Sonnet 4.6 --- cmd/cli/commands/context.go | 50 ++++++++++++++++++-------------- cmd/cli/commands/context_test.go | 45 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/cmd/cli/commands/context.go b/cmd/cli/commands/context.go index 9a1843cc9..2ddd4bef3 100644 --- a/cmd/cli/commands/context.go +++ b/cmd/cli/commands/context.go @@ -17,7 +17,6 @@ import ( "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/docker/model-runner/pkg/envconfig" "github.com/spf13/cobra" ) @@ -76,15 +75,38 @@ 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 it falls back to generic strings. +// 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 } @@ -93,26 +115,10 @@ func resolveDefaultContext(ctx context.Context) (host, description string) { defer cancel() kind := desktop.DetectEngineKind(detectCtx, dockerCLI) - description = "Model Runner on " + kind.String() - - switch kind { - case types.ModelRunnerEngineKindDesktop: - host = kind.String() - case types.ModelRunnerEngineKindCloud: - if envconfig.TLSEnabled() { - host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortCloud) - } else { - host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortCloud) - } - case types.ModelRunnerEngineKindMoby, types.ModelRunnerEngineKindMobyManual: - if envconfig.TLSEnabled() { - host = "https://localhost:" + strconv.Itoa(standalone.DefaultTLSPortMoby) - } else { - host = "http://localhost:" + strconv.Itoa(standalone.DefaultControllerPortMoby) - } - } - - return host, description + // 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. diff --git a/cmd/cli/commands/context_test.go b/cmd/cli/commands/context_test.go index 7522c5822..0850e093e 100644 --- a/cmd/cli/commands/context_test.go +++ b/cmd/cli/commands/context_test.go @@ -2,11 +2,15 @@ package commands import ( "bytes" + "context" "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" ) @@ -395,3 +399,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(context.Background()) + assert.Equal(t, "(auto-detect)", host) + assert.Equal(t, "Auto-detected Docker context", desc) +} From 1f622535e6bcd4e48cb8acdd312205c63499a171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Thu, 2 Jul 2026 12:03:11 +0200 Subject: [PATCH 5/6] fix: use t.Context() instead of context.Background() in test Satisfies the forbidigo lint rule that forbids context.Background() in tests. Co-Authored-By: Claude Sonnet 4.6 --- cmd/cli/commands/context_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/cli/commands/context_test.go b/cmd/cli/commands/context_test.go index 0850e093e..b04279870 100644 --- a/cmd/cli/commands/context_test.go +++ b/cmd/cli/commands/context_test.go @@ -2,7 +2,6 @@ package commands import ( "bytes" - "context" "encoding/json" "strconv" "strings" @@ -436,7 +435,7 @@ func TestResolveKindHost(t *testing.T) { // any network probe. func TestResolveDefaultContext_nilCLI(t *testing.T) { dockerCLI = nil - host, desc := resolveDefaultContext(context.Background()) + host, desc := resolveDefaultContext(t.Context()) assert.Equal(t, "(auto-detect)", host) assert.Equal(t, "Auto-detected Docker context", desc) } From b2312f561f323d07ef5e8c8a2b0abd2f5a2f9575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacio=20L=C3=B3pez=20Luna?= Date: Fri, 3 Jul 2026 20:55:50 +0200 Subject: [PATCH 6/6] fix: restore dockerCLI global after TestResolveDefaultContext_nilCLI Save the previous value and defer its restoration via t.Cleanup to prevent state leakage into other tests. Co-Authored-By: Claude Sonnet 4.6 --- cmd/cli/commands/context_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/cli/commands/context_test.go b/cmd/cli/commands/context_test.go index b04279870..e99d37407 100644 --- a/cmd/cli/commands/context_test.go +++ b/cmd/cli/commands/context_test.go @@ -434,6 +434,8 @@ func TestResolveKindHost(t *testing.T) { // dockerCLI is not set, the fallback strings are returned immediately without // any network probe. func TestResolveDefaultContext_nilCLI(t *testing.T) { + prev := dockerCLI + t.Cleanup(func() { dockerCLI = prev }) dockerCLI = nil host, desc := resolveDefaultContext(t.Context()) assert.Equal(t, "(auto-detect)", host)