diff --git a/cmd/cli/commands/context.go b/cmd/cli/commands/context.go index 4a1d1489c..9a1843cc9 100644 --- a/cmd/cli/commands/context.go +++ b/cmd/cli/commands/context.go @@ -2,19 +2,29 @@ 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/docker/model-runner/pkg/envconfig" "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 +76,45 @@ 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: + 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 +} + // newContextCreateCmd returns the "context create" command. func newContextCreateCmd() *cobra.Command { var ( @@ -223,12 +272,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, }, } @@ -325,15 +378,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 diff --git a/cmd/cli/desktop/context.go b/cmd/cli/desktop/context.go index 7e6546b2c..17a059ffe 100644 --- a/cmd/cli/desktop/context.go +++ b/cmd/cli/desktop/context.go @@ -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 +} + // 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.