2.0.1 dcf7d6b

factory.go RawHistoryFolder

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}

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