1package container
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/user"
8 "strings"
9
10 "github.com/docker/docker/client"
11 "github.com/rs/zerolog/log"
12
13 "gordon/internal/config"
14 "gordon/pkg/runtime"
15)
16
17// RuntimeType represents the container runtime type
18type RuntimeType string
19
20const (
21 RuntimeAuto RuntimeType = "auto"
22 RuntimeDocker RuntimeType = "docker"
23 RuntimePodman RuntimeType = "podman"
24 RuntimePodmanRootless RuntimeType = "podman-rootless"
25)
26
27// RuntimeInfo contains information about a detected runtime
28type RuntimeInfo struct {
29 Type RuntimeType
30 SocketPath string
31 Version string
32}
33
34// CreateRuntime creates a container runtime based on configuration
35func CreateRuntime(ctx context.Context, cfg *config.Config) (runtime.Runtime, error) {
36 runtimeType := RuntimeType(cfg.Server.Runtime)
37
38 // Get socket path from config or environment
39 socketPath := getSocketPath(cfg.Server.SocketPath)
40
41 switch runtimeType {
42 case RuntimeAuto:
43 return createAutoRuntime(ctx, socketPath)
44 case RuntimeDocker:
45 return createDockerRuntime(ctx, socketPath)
46 case RuntimePodman, RuntimePodmanRootless:
47 return createPodmanRuntime(ctx, socketPath, runtimeType == RuntimePodmanRootless)
48 default:
49 return nil, fmt.Errorf("unsupported runtime: %s", runtimeType)
50 }
51}
52
53// getSocketPath determines the socket path to use
54func getSocketPath(configPath string) string {
55 // 1. Use config if provided
56 if configPath != "" {
57 log.Debug().Str("path", configPath).Msg("Using socket path from config")
58 return configPath
59 }
60
61 // 2. Check CONTAINER_HOST environment variable
62 if envPath := os.Getenv("CONTAINER_HOST"); envPath != "" {
63 log.Debug().Str("path", envPath).Msg("Using socket path from CONTAINER_HOST")
64 return envPath
65 }
66
67 // 3. Return empty string for auto-detection
68 return ""
69}
70
71// createAutoRuntime automatically detects and creates the best available runtime
72func createAutoRuntime(ctx context.Context, customSocketPath string) (runtime.Runtime, error) {
73 var candidatePaths []string
74
75 if customSocketPath != "" {
76 // If custom path provided, try it first
77 candidatePaths = []string{customSocketPath}
78 } else {
79 // Auto-detect socket paths
80 candidatePaths = getDefaultSocketPaths()
81 }
82
83 // Try each socket path
84 for _, socketPath := range candidatePaths {
85 if rt, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
86 log.Info().
87 Str("runtime", string(info.Type)).
88 Str("socket", info.SocketPath).
89 Str("version", info.Version).
90 Msg("Container runtime auto-detected")
91 return rt, nil
92 }
93 }
94
95 return nil, fmt.Errorf("no container runtime found. Tried paths: %v", candidatePaths)
96}
97
98// createDockerRuntime creates a Docker runtime with optional custom socket
99func createDockerRuntime(ctx context.Context, socketPath string) (runtime.Runtime, error) {
100 var opts []client.Opt
101 opts = append(opts, client.FromEnv, client.WithAPIVersionNegotiation())
102
103 if socketPath != "" {
104 opts = append(opts, client.WithHost(socketPath))
105 }
106
107 cli, err := client.NewClientWithOpts(opts...)
108 if err != nil {
109 return nil, fmt.Errorf("failed to create Docker client: %w", err)
110 }
111
112 // Test connectivity
113 if _, err := cli.Ping(ctx); err != nil {
114 return nil, fmt.Errorf("Docker ping failed: %w", err)
115 }
116
117 return &DockerRuntime{client: cli}, nil
118}
119
120// createPodmanRuntime creates a Podman runtime with optional custom socket
121func createPodmanRuntime(ctx context.Context, socketPath string, rootless bool) (runtime.Runtime, error) {
122 if socketPath == "" {
123 socketPath = getDefaultPodmanSocket(rootless)
124 }
125
126 return createPodmanRuntimeWithSocket(ctx, socketPath)
127}
128
129// tryCreateRuntime attempts to create a runtime for a given socket path
130func tryCreateRuntime(ctx context.Context, socketPath string) (runtime.Runtime, *RuntimeInfo, error) {
131 // Try to create client
132 opts := []client.Opt{
133 client.WithHost(socketPath),
134 client.WithAPIVersionNegotiation(),
135 }
136
137 cli, err := client.NewClientWithOpts(opts...)
138 if err != nil {
139 return nil, nil, fmt.Errorf("failed to create client: %w", err)
140 }
141
142 // Test connectivity and get version
143 ping, err := cli.Ping(ctx)
144 if err != nil {
145 return nil, nil, fmt.Errorf("ping failed: %w", err)
146 }
147
148 // Determine runtime type based on socket path and server info
149 runtimeType := detectRuntimeType(socketPath)
150
151 // Since Podman uses Docker-compatible API, we can use DockerRuntime for both
152 rt := &DockerRuntime{client: cli}
153
154 info := &RuntimeInfo{
155 Type: runtimeType,
156 SocketPath: socketPath,
157 Version: ping.APIVersion,
158 }
159
160 return rt, info, nil
161}
162
163// detectRuntimeType determines if the socket is Docker or Podman
164func detectRuntimeType(socketPath string) RuntimeType {
165 // Check socket path patterns
166 if strings.Contains(socketPath, "podman") {
167 if strings.Contains(socketPath, "/run/user/") || strings.Contains(socketPath, "$XDG_RUNTIME_DIR") {
168 return RuntimePodmanRootless
169 }
170 return RuntimePodman
171 }
172
173 if strings.Contains(socketPath, "docker") {
174 return RuntimeDocker
175 }
176
177 // Default socket paths
178 switch socketPath {
179 case "/var/run/docker.sock", "unix:///var/run/docker.sock":
180 return RuntimeDocker
181 case "/run/podman/podman.sock", "unix:///run/podman/podman.sock":
182 return RuntimePodman
183 }
184
185 // Check if it's in user runtime directory (rootless)
186 if strings.Contains(socketPath, "/run/user/") {
187 return RuntimePodmanRootless
188 }
189
190 // Default to Docker for unknown paths
191 return RuntimeDocker
192}
193
194// getDefaultSocketPaths returns the default socket paths to try
195func getDefaultSocketPaths() []string {
196 paths := []string{
197 "unix:///var/run/docker.sock", // Docker
198 }
199
200 // Add Podman rootless socket first (preferred)
201 if rootlessSocket := getDefaultPodmanSocket(true); rootlessSocket != "" {
202 paths = append(paths, rootlessSocket)
203 }
204
205 // Add Podman root socket last
206 paths = append(paths, "unix:///run/podman/podman.sock")
207
208 return paths
209}
210
211// getDefaultPodmanSocket returns the default Podman socket path
212func getDefaultPodmanSocket(rootless bool) string {
213 if !rootless {
214 return "unix:///run/podman/podman.sock"
215 }
216
217 // Get user runtime directory for rootless Podman
218 if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
219 return fmt.Sprintf("unix://%s/podman/podman.sock", xdgRuntimeDir)
220 }
221
222 // Fallback: construct from user ID
223 if currentUser, err := user.Current(); err == nil {
224 return fmt.Sprintf("unix:///run/user/%s/podman/podman.sock", currentUser.Uid)
225 }
226
227 log.Warn().Msg("Could not determine rootless Podman socket path")
228 return ""
229}
230
231// createPodmanRuntimeWithSocket creates a Podman runtime for a specific socket
232func createPodmanRuntimeWithSocket(ctx context.Context, socketPath string) (runtime.Runtime, error) {
233 opts := []client.Opt{
234 client.WithHost(socketPath),
235 client.WithAPIVersionNegotiation(),
236 }
237
238 cli, err := client.NewClientWithOpts(opts...)
239 if err != nil {
240 return nil, fmt.Errorf("failed to create Podman client: %w", err)
241 }
242
243 // Test connectivity
244 if _, err := cli.Ping(ctx); err != nil {
245 return nil, fmt.Errorf("Podman ping failed: %w", err)
246 }
247
248 return &DockerRuntime{client: cli}, nil
249}
250
251// GetAvailableRuntimes returns information about all available runtimes
252func GetAvailableRuntimes() []RuntimeInfo {
253 var runtimes []RuntimeInfo
254 ctx := context.Background()
255
256 for _, socketPath := range getDefaultSocketPaths() {
257 if _, info, err := tryCreateRuntime(ctx, socketPath); err == nil {
258 runtimes = append(runtimes, *info)
259 }
260 }
261
262 return runtimes
263}