Skip to content

Adding Colima Runtime Support #1492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
16 changes: 13 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/thv.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion docs/proposals/thvignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

---

Expand Down Expand Up @@ -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 |

34 changes: 34 additions & 0 deletions pkg/container/docker/sdk/client_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions pkg/container/docker/sdk/client_unix_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
6 changes: 5 additions & 1 deletion pkg/container/docker/sdk/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/container/runtime/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
59 changes: 59 additions & 0 deletions pkg/container/runtime/types_test.go
Original file line number Diff line number Diff line change
@@ -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)
}