From 7818fa6136f55befe2c9f03f263b318ae1db3239 Mon Sep 17 00:00:00 2001 From: Jesse O'Brien Date: Tue, 19 Aug 2025 09:37:09 -0400 Subject: [PATCH 1/2] Adding colima support to existing docker runtime searches - Respects the default colima socket location - TOOLHIVE_COLIMA_SOCKET=.colima/default/docker.sock - This is documented here: https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#v040-or-newer - Adds test coverage for new colima functionality --- pkg/container/docker/sdk/client_unix.go | 34 +++++++++++ pkg/container/docker/sdk/client_unix_test.go | 62 ++++++++++++++++++++ pkg/container/docker/sdk/factory.go | 6 +- pkg/container/runtime/types.go | 2 + pkg/container/runtime/types_test.go | 59 +++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 pkg/container/docker/sdk/client_unix_test.go create mode 100644 pkg/container/runtime/types_test.go diff --git a/pkg/container/docker/sdk/client_unix.go b/pkg/container/docker/sdk/client_unix.go index 464db850f..10101c450 100644 --- a/pkg/container/docker/sdk/client_unix.go +++ b/pkg/container/docker/sdk/client_unix.go @@ -53,6 +53,15 @@ func findPlatformContainerSocket(rt runtime.Type) (string, runtime.Type, error) return customSocketPath, runtime.TypePodman, nil } + if customSocketPath := os.Getenv(ColimaSocketEnv); customSocketPath != "" { + logger.Debugf("Using Colima socket from env: %s", customSocketPath) + // validate the socket path + if _, err := os.Stat(customSocketPath); err != nil { + return "", runtime.TypeColima, fmt.Errorf("invalid Colima socket path: %w", err) + } + return customSocketPath, runtime.TypeColima, nil + } + if customSocketPath := os.Getenv(DockerSocketEnv); customSocketPath != "" { logger.Debugf("Using Docker socket from env: %s", customSocketPath) // validate the socket path @@ -69,6 +78,13 @@ func findPlatformContainerSocket(rt runtime.Type) (string, runtime.Type, error) } } + if rt == runtime.TypeColima { + socketPath, err := findColimaSocket() + if err == nil { + return socketPath, runtime.TypeColima, nil + } + } + if rt == runtime.TypeDocker { socketPath, err := findDockerSocket() if err == nil { @@ -119,6 +135,24 @@ func findPodmanSocket() (string, error) { return "", fmt.Errorf("podman socket not found in standard locations") } +// findColimaSocket attempts to locate a Colima socket +func findColimaSocket() (string, error) { + // Check user-specific location for Colima + if home := os.Getenv("HOME"); home != "" { + colimaSocketPath := filepath.Join(home, ColimaSocketPath) + _, err := os.Stat(colimaSocketPath) + + if err == nil { + logger.Debugf("Found Colima socket at %s", colimaSocketPath) + return colimaSocketPath, nil + } + + logger.Debugf("Failed to check Colima socket at %s: %v", colimaSocketPath, err) + } + + return "", fmt.Errorf("colima socket not found in standard locations") +} + // findDockerSocket attempts to locate a Docker socket func findDockerSocket() (string, error) { // Try Docker socket as fallback diff --git a/pkg/container/docker/sdk/client_unix_test.go b/pkg/container/docker/sdk/client_unix_test.go new file mode 100644 index 000000000..19295dd60 --- /dev/null +++ b/pkg/container/docker/sdk/client_unix_test.go @@ -0,0 +1,62 @@ +//go:build !windows +// +build !windows + +package sdk + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stacklok/toolhive/pkg/container/runtime" + "github.com/stacklok/toolhive/pkg/logger" +) + +func init() { + // Initialize the logger for tests + logger.Initialize() +} + +func TestColimaRuntimeTypeSupport(t *testing.T) { + t.Parallel() + + // Test that Colima is included in supported socket paths + found := false + for _, rt := range supportedSocketPaths { + if rt == runtime.TypeColima { + found = true + break + } + } + + require.True(t, found, "TypeColima should be included in supportedSocketPaths") +} + +func TestColimaConstants(t *testing.T) { + t.Parallel() + + // Test that Colima constants are properly defined + assert.Equal(t, "TOOLHIVE_COLIMA_SOCKET", ColimaSocketEnv) + assert.Equal(t, ".colima/default/docker.sock", ColimaSocketPath) + assert.Equal(t, runtime.Type("colima"), runtime.TypeColima) +} + +func TestNewDockerClientWithColima(t *testing.T) { + t.Parallel() + + // This test verifies that the NewDockerClient function can handle + // the Colima runtime type in the supportedSocketPaths without errors + // Note: This test won't actually connect since no container runtime is available + // but it verifies the code paths don't panic and handle the new runtime type + + ctx := context.Background() + _, _, _, err := NewDockerClient(ctx) + + // We expect an error since no container runtime is available in the test environment + // but we're testing that the function doesn't panic or have compile errors + // with the new Colima support + assert.Error(t, err) + assert.Contains(t, err.Error(), "no supported container runtime") +} diff --git a/pkg/container/docker/sdk/factory.go b/pkg/container/docker/sdk/factory.go index ec940c0f6..ae90f4b32 100644 --- a/pkg/container/docker/sdk/factory.go +++ b/pkg/container/docker/sdk/factory.go @@ -22,6 +22,8 @@ const ( DockerSocketEnv = "TOOLHIVE_DOCKER_SOCKET" // PodmanSocketEnv is the environment variable for custom Podman socket path PodmanSocketEnv = "TOOLHIVE_PODMAN_SOCKET" + // ColimaSocketEnv is the environment variable for custom Colima socket path + ColimaSocketEnv = "TOOLHIVE_COLIMA_SOCKET" ) // Common socket paths @@ -32,13 +34,15 @@ const ( PodmanXDGRuntimeSocketPath = "podman/podman.sock" // DockerSocketPath is the default Docker socket path DockerSocketPath = "/var/run/docker.sock" + // ColimaSocketPath is the default Colima socket path + ColimaSocketPath = ".colima/default/docker.sock" // DockerDesktopMacSocketPath is the Docker Desktop socket path on macOS DockerDesktopMacSocketPath = ".docker/run/docker.sock" // RancherDesktopMacSocketPath is the Docker socket path for Rancher Desktop on macOS RancherDesktopMacSocketPath = ".rd/docker.sock" ) -var supportedSocketPaths = []runtime.Type{runtime.TypePodman, runtime.TypeDocker} +var supportedSocketPaths = []runtime.Type{runtime.TypePodman, runtime.TypeColima, runtime.TypeDocker} // NewDockerClient creates a new container client func NewDockerClient(ctx context.Context) (*client.Client, string, runtime.Type, error) { diff --git a/pkg/container/runtime/types.go b/pkg/container/runtime/types.go index 393c3032e..00e279b6c 100644 --- a/pkg/container/runtime/types.go +++ b/pkg/container/runtime/types.go @@ -184,6 +184,8 @@ const ( TypePodman Type = "podman" // TypeDocker represents the Docker runtime TypeDocker Type = "docker" + // TypeColima represents the Colima runtime + TypeColima Type = "colima" // TypeKubernetes represents the Kubernetes runtime TypeKubernetes Type = "kubernetes" ) diff --git a/pkg/container/runtime/types_test.go b/pkg/container/runtime/types_test.go new file mode 100644 index 000000000..a595e6efe --- /dev/null +++ b/pkg/container/runtime/types_test.go @@ -0,0 +1,59 @@ +package runtime + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRuntimeTypes(t *testing.T) { + t.Parallel() + + // Test that all runtime types are properly defined + tests := []struct { + name string + runtimeType Type + expectedValue string + }{ + { + name: "TypePodman", + runtimeType: TypePodman, + expectedValue: "podman", + }, + { + name: "TypeDocker", + runtimeType: TypeDocker, + expectedValue: "docker", + }, + { + name: "TypeColima", + runtimeType: TypeColima, + expectedValue: "colima", + }, + { + name: "TypeKubernetes", + runtimeType: TypeKubernetes, + expectedValue: "kubernetes", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.expectedValue, string(tt.runtimeType)) + }) + } +} + +func TestColimaRuntimeTypeExists(t *testing.T) { + t.Parallel() + + // Ensure TypeColima constant exists and has the correct value + assert.Equal(t, Type("colima"), TypeColima) + + // Verify it's different from other runtime types + assert.NotEqual(t, TypeColima, TypeDocker) + assert.NotEqual(t, TypeColima, TypePodman) + assert.NotEqual(t, TypeColima, TypeKubernetes) +} From ae8a108d39fdbd7e5717b62c95352cdf1d392711 Mon Sep 17 00:00:00 2001 From: Jesse O'Brien Date: Mon, 11 Aug 2025 15:14:31 -0400 Subject: [PATCH 2/2] Updating documentation with colima references --- CLAUDE.md | 16 +++++++++++++--- docs/cli/thv.md | 2 +- docs/proposals/thvignore.md | 3 ++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ab4501cbb..15f9abd54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ ToolHive is a lightweight, secure manager for MCP (Model Context Protocol: https - **Kubernetes Operator (`thv-operator`)**: Manages MCP servers in Kubernetes clusters - **Proxy Runner (`thv-proxyrunner`)**: Handles proxy functionality for MCP server communication -The application acts as a thin client for Docker/Podman Unix socket API, providing container-based isolation for running MCP servers securely. It also builds on top of the MCP Specification: https://modelcontextprotocol.io/specification. +The application acts as a thin client for Docker/Podman/Colima Unix socket API, providing container-based isolation for running MCP servers securely. It also builds on top of the MCP Specification: https://modelcontextprotocol.io/specification. ## Build and Development Commands @@ -90,7 +90,7 @@ The test framework uses Ginkgo and Gomega for BDD-style testing. ### Key Design Patterns -- **Factory Pattern**: Used extensively for creating runtime-specific implementations (Docker vs Kubernetes) +- **Factory Pattern**: Used extensively for creating runtime-specific implementations (Docker/Colima/Podman vs Kubernetes) - **Interface Segregation**: Clean abstractions for container runtimes, transports, and storage - **Middleware Pattern**: HTTP middleware for auth, authz, telemetry - **Observer Pattern**: Event system for audit logging @@ -131,6 +131,16 @@ The project uses `go.uber.org/mock` for generating mocks. Mock files are located - Supports environment variable overrides - Client configuration stored in `~/.toolhive/` or equivalent +### Container Runtime Configuration + +ToolHive automatically detects available container runtimes in the following order: Podman, Colima, Docker. You can override the default socket paths using environment variables: + +- `TOOLHIVE_PODMAN_SOCKET`: Custom Podman socket path +- `TOOLHIVE_COLIMA_SOCKET`: Custom Colima socket path (default: `~/.colima/default/docker.sock`) +- `TOOLHIVE_DOCKER_SOCKET`: Custom Docker socket path + +**Colima Support**: Colima is fully supported as a Docker-compatible runtime. ToolHive will automatically detect Colima installations on macOS and Linux systems. + ## Development Guidelines ### Code Organization @@ -176,7 +186,7 @@ When working on the Kubernetes operator: ### Working with Containers -The container abstraction supports both Docker and Kubernetes runtimes. When adding container functionality: +The container abstraction supports Docker, Colima, Podman, and Kubernetes runtimes. When adding container functionality: - Implement the interface in `pkg/container/runtime/types.go` - Add runtime-specific implementations in appropriate subdirectories - Use factory pattern for runtime selection diff --git a/docs/cli/thv.md b/docs/cli/thv.md index fdd48eabc..925df1619 100644 --- a/docs/cli/thv.md +++ b/docs/cli/thv.md @@ -18,7 +18,7 @@ ToolHive (thv) is a lightweight, secure, and fast manager for MCP servers ToolHive (thv) is a lightweight, secure, and fast manager for MCP (Model Context Protocol) servers. It is written in Go and has extensive test coverage—including input validation—to ensure reliability and security. -Under the hood, ToolHive acts as a very thin client for the Docker/Podman Unix socket API. +Under the hood, ToolHive acts as a very thin client for the Docker/Podman/Colima Unix socket API. This design choice allows it to remain both efficient and lightweight while still providing powerful, container-based isolation for running MCP servers. diff --git a/docs/proposals/thvignore.md b/docs/proposals/thvignore.md index 520a4415e..077d2ffa0 100644 --- a/docs/proposals/thvignore.md +++ b/docs/proposals/thvignore.md @@ -169,6 +169,7 @@ func convertMounts(mounts []runtime.Mount) []mount.Mount { | ----- | ----- | ----- | | Docker | ✅ `mount.TypeBind` | ✅ `mount.TypeTmpfs` | | Podman | ✅ `--mount type=bind` | ✅ `--mount type=tmpfs` | +| Colima | ✅ `mount.TypeBind` | ✅ `mount.TypeTmpfs` | --- @@ -341,6 +342,6 @@ docker run \ | Real-time file access | ✅ via full bind mount | | Hidden files (e.g. `.ssh`, `.env`) | ✅ overlaid with tmpfs | | Config flexibility | ✅ per-folder \+ global `.thvignore` | -| Runtime compatibility | ✅ Docker, Podman | +| Runtime compatibility | ✅ Docker, Podman, Colima | | Integration | ✅ Works with existing permission profiles |