v2.0.0 ae3e752

factory.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close