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