2.0.1 dcf7d6b

podman.go RawHistoryFolder

1package container
2
3import (
4	"context"
5	"fmt"
6	"io"
7	"strconv"
8	"strings"
9
10	"github.com/docker/docker/api/types/container"
11	"github.com/docker/docker/api/types/image"
12	"github.com/docker/docker/client"
13	"github.com/docker/go-connections/nat"
14	"github.com/rs/zerolog/log"
15
16	"gordon/pkg/runtime"
17)
18
19// PodmanRuntime implements the Runtime interface using Podman API
20// Podman API is compatible with Docker API, so we can reuse the Docker client
21type PodmanRuntime struct {
22	client *client.Client
23}
24
25// NewPodmanRuntime creates a new Podman runtime instance
26func NewPodmanRuntime() (*PodmanRuntime, error) {
27	// Try to create with auto-detection
28	socketPath := getDefaultPodmanSocket(false) // Try root first
29	rt, err := createPodmanRuntimeWithSocket(socketPath)
30	if err != nil {
31		// Try rootless
32		socketPath = getDefaultPodmanSocket(true)
33		rt, err = createPodmanRuntimeWithSocket(socketPath)
34		if err != nil {
35			return nil, fmt.Errorf("failed to create Podman runtime: %w", err)
36		}
37	}
38	
39	return rt.(*PodmanRuntime), nil
40}
41
42// CreateContainer creates a new container
43func (p *PodmanRuntime) CreateContainer(ctx context.Context, config *runtime.ContainerConfig) (*runtime.Container, error) {
44	// Convert ports to Podman/Docker format
45	exposedPorts := make(nat.PortSet)
46	portBindings := make(nat.PortMap)
47	
48	for _, port := range config.Ports {
49		containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
50		exposedPorts[containerPort] = struct{}{}
51		
52		// Bind to random available port on host
53		portBindings[containerPort] = []nat.PortBinding{
54			{
55				HostIP:   "0.0.0.0",
56				HostPort: "0", // Podman will assign a random available port
57			},
58		}
59	}
60
61	// Create container configuration
62	containerConfig := &container.Config{
63		Image:        config.Image,
64		Env:          config.Env,
65		ExposedPorts: exposedPorts,
66		WorkingDir:   config.WorkingDir,
67		Cmd:          config.Cmd,
68		Labels:       config.Labels,
69	}
70
71	hostConfig := &container.HostConfig{
72		PortBindings: portBindings,
73		AutoRemove:   config.AutoRemove,
74	}
75
76	// Create the container
77	resp, err := p.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, config.Name)
78	if err != nil {
79		return nil, fmt.Errorf("failed to create container: %w", err)
80	}
81
82	log.Info().Str("id", resp.ID).Str("name", config.Name).Str("image", config.Image).Msg("Podman container created")
83
84	// Inspect the created container to get full details
85	return p.InspectContainer(ctx, resp.ID)
86}
87
88// StartContainer starts a container
89func (p *PodmanRuntime) StartContainer(ctx context.Context, containerID string) error {
90	err := p.client.ContainerStart(ctx, containerID, container.StartOptions{})
91	if err != nil {
92		return fmt.Errorf("failed to start container %s: %w", containerID, err)
93	}
94
95	log.Info().Str("id", containerID).Msg("Podman container started")
96	return nil
97}
98
99// StopContainer stops a container
100func (p *PodmanRuntime) StopContainer(ctx context.Context, containerID string) error {
101	timeout := 30 // 30 seconds timeout
102	err := p.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
103	if err != nil {
104		return fmt.Errorf("failed to stop container %s: %w", containerID, err)
105	}
106
107	log.Info().Str("id", containerID).Msg("Podman container stopped")
108	return nil
109}
110
111// RestartContainer restarts a container
112func (p *PodmanRuntime) RestartContainer(ctx context.Context, containerID string) error {
113	timeout := 30 // 30 seconds timeout
114	err := p.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout})
115	if err != nil {
116		return fmt.Errorf("failed to restart container %s: %w", containerID, err)
117	}
118
119	log.Info().Str("id", containerID).Msg("Podman container restarted")
120	return nil
121}
122
123// RemoveContainer removes a container
124func (p *PodmanRuntime) RemoveContainer(ctx context.Context, containerID string, force bool) error {
125	err := p.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force})
126	if err != nil {
127		return fmt.Errorf("failed to remove container %s: %w", containerID, err)
128	}
129
130	log.Info().Str("id", containerID).Bool("force", force).Msg("Podman container removed")
131	return nil
132}
133
134// ListContainers lists containers
135func (p *PodmanRuntime) ListContainers(ctx context.Context, all bool) ([]*runtime.Container, error) {
136	containers, err := p.client.ContainerList(ctx, container.ListOptions{All: all})
137	if err != nil {
138		return nil, fmt.Errorf("failed to list containers: %w", err)
139	}
140
141	var result []*runtime.Container
142	for _, c := range containers {
143		// Extract ports
144		var ports []int
145		for _, port := range c.Ports {
146			if port.PublicPort > 0 {
147				ports = append(ports, int(port.PublicPort))
148			}
149		}
150
151		// Get the primary name (remove leading slash)
152		name := ""
153		if len(c.Names) > 0 {
154			name = strings.TrimPrefix(c.Names[0], "/")
155		}
156
157		result = append(result, &runtime.Container{
158			ID:     c.ID,
159			Image:  c.Image,
160			Name:   name,
161			Status: c.Status,
162			Ports:  ports,
163			Labels: c.Labels,
164		})
165	}
166
167	return result, nil
168}
169
170// InspectContainer inspects a container
171func (p *PodmanRuntime) InspectContainer(ctx context.Context, containerID string) (*runtime.Container, error) {
172	resp, err := p.client.ContainerInspect(ctx, containerID)
173	if err != nil {
174		return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
175	}
176
177	// Extract published ports
178	var ports []int
179	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
180		for _, bindings := range resp.NetworkSettings.Ports {
181			for _, binding := range bindings {
182				if binding.HostPort != "" {
183					if port, err := strconv.Atoi(binding.HostPort); err == nil {
184						ports = append(ports, port)
185					}
186				}
187			}
188		}
189	}
190
191	// Get container name (remove leading slash)
192	name := strings.TrimPrefix(resp.Name, "/")
193
194	return &runtime.Container{
195		ID:     resp.ID,
196		Image:  resp.Config.Image,
197		Name:   name,
198		Status: resp.State.Status,
199		Ports:  ports,
200		Labels: resp.Config.Labels,
201	}, nil
202}
203
204// GetContainerLogs gets container logs
205func (p *PodmanRuntime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
206	logs, err := p.client.ContainerLogs(ctx, containerID, container.LogsOptions{
207		ShowStdout: true,
208		ShowStderr: true,
209		Follow:     follow,
210		Timestamps: true,
211	})
212	if err != nil {
213		return nil, fmt.Errorf("failed to get logs for container %s: %w", containerID, err)
214	}
215
216	return logs, nil
217}
218
219// PullImage pulls an image
220func (p *PodmanRuntime) PullImage(ctx context.Context, imageRef string) error {
221	log.Info().Str("image", imageRef).Msg("Pulling image with Podman")
222	
223	reader, err := p.client.ImagePull(ctx, imageRef, image.PullOptions{})
224	if err != nil {
225		return fmt.Errorf("failed to pull image %s: %w", imageRef, err)
226	}
227	defer reader.Close()
228
229	// Read the response to completion (this is required for the pull to complete)
230	_, err = io.Copy(io.Discard, reader)
231	if err != nil {
232		return fmt.Errorf("failed to read pull response for image %s: %w", imageRef, err)
233	}
234
235	log.Info().Str("image", imageRef).Msg("Image pulled successfully with Podman")
236	return nil
237}
238
239// RemoveImage removes an image
240func (p *PodmanRuntime) RemoveImage(ctx context.Context, imageRef string, force bool) error {
241	_, err := p.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force})
242	if err != nil {
243		return fmt.Errorf("failed to remove image %s: %w", imageRef, err)
244	}
245
246	log.Info().Str("image", imageRef).Bool("force", force).Msg("Podman image removed")
247	return nil
248}
249
250// ListImages lists images
251func (p *PodmanRuntime) ListImages(ctx context.Context) ([]string, error) {
252	images, err := p.client.ImageList(ctx, image.ListOptions{})
253	if err != nil {
254		return nil, fmt.Errorf("failed to list images: %w", err)
255	}
256
257	var result []string
258	for _, img := range images {
259		for _, tag := range img.RepoTags {
260			if tag != "<none>:<none>" {
261				result = append(result, tag)
262			}
263		}
264	}
265
266	return result, nil
267}
268
269// Ping checks if Podman is responsive
270func (p *PodmanRuntime) Ping(ctx context.Context) error {
271	_, err := p.client.Ping(ctx)
272	if err != nil {
273		return fmt.Errorf("Podman ping failed: %w", err)
274	}
275	return nil
276}
277
278// Version returns Podman version
279func (p *PodmanRuntime) Version(ctx context.Context) (string, error) {
280	version, err := p.client.ServerVersion(ctx)
281	if err != nil {
282		return "", fmt.Errorf("failed to get Podman version: %w", err)
283	}
284	return version.Version, nil
285}
286
287// IsContainerRunning checks if a container is running
288func (p *PodmanRuntime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) {
289	container, err := p.InspectContainer(ctx, containerID)
290	if err != nil {
291		return false, err
292	}
293	return container.Status == "running", nil
294}
295
296// GetContainerPort gets the host port for a container's internal port
297func (p *PodmanRuntime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) {
298	resp, err := p.client.ContainerInspect(ctx, containerID)
299	if err != nil {
300		return 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
301	}
302
303	if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil {
304		return 0, fmt.Errorf("no port mappings found for container %s", containerID)
305	}
306
307	containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort))
308	bindings, exists := resp.NetworkSettings.Ports[containerPort]
309	if !exists || len(bindings) == 0 {
310		return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID)
311	}
312
313	hostPort, err := strconv.Atoi(bindings[0].HostPort)
314	if err != nil {
315		return 0, fmt.Errorf("invalid host port for container %s: %w", containerID, err)
316	}
317
318	return hostPort, nil
319}
320
321// GetImageExposedPorts gets the exposed ports from an image
322func (p *PodmanRuntime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) {
323	imageInspect, err := p.client.ImageInspect(ctx, imageRef)
324	if err != nil {
325		return nil, fmt.Errorf("failed to inspect image %s: %w", imageRef, err)
326	}
327
328	var ports []int
329	if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil {
330		for portSpec := range imageInspect.Config.ExposedPorts {
331			// Parse port from format like "80/tcp"
332			portStr := strings.Split(string(portSpec), "/")[0]
333			if port, err := strconv.Atoi(portStr); err == nil {
334				ports = append(ports, port)
335			}
336		}
337	}
338
339	// If no exposed ports found, return common web ports as fallback
340	if len(ports) == 0 {
341		log.Warn().Str("image", imageRef).Msg("No EXPOSE directives found in image, using common web ports")
342		return []int{80, 8080, 3000}, nil
343	}
344
345	log.Info().Str("image", imageRef).Ints("exposed_ports", ports).Msg("Found exposed ports in image")
346	return ports, nil
347}
348
349// GetContainerExposedPorts gets all exposed ports from a running container
350func (p *PodmanRuntime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) {
351	resp, err := p.client.ContainerInspect(ctx, containerID)
352	if err != nil {
353		return nil, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
354	}
355
356	var ports []int
357	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
358		for portSpec := range resp.NetworkSettings.Ports {
359			// Parse port from format like "80/tcp"
360			portStr := strings.Split(string(portSpec), "/")[0]
361			if port, err := strconv.Atoi(portStr); err == nil {
362				ports = append(ports, port)
363			}
364		}
365	}
366
367	return ports, nil
368}
369
370// GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use
371func (p *PodmanRuntime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) {
372	resp, err := p.client.ContainerInspect(ctx, containerID)
373	if err != nil {
374		return "", 0, fmt.Errorf("failed to inspect container %s: %w", containerID, err)
375	}
376
377	// Get container's internal IP address
378	var containerIP string
379	if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil {
380		// Use the first available network (usually bridge or custom network)
381		for _, network := range resp.NetworkSettings.Networks {
382			if network.IPAddress != "" {
383				containerIP = network.IPAddress
384				break
385			}
386		}
387	}
388
389	if containerIP == "" {
390		return "", 0, fmt.Errorf("no IP address found for container %s", containerID)
391	}
392
393	// Get exposed ports and select the best one (Traefik-style logic)
394	var exposedPorts []int
395	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
396		for portSpec := range resp.NetworkSettings.Ports {
397			// Parse port from format like "80/tcp"
398			portStr := strings.Split(string(portSpec), "/")[0]
399			if port, err := strconv.Atoi(portStr); err == nil {
400				exposedPorts = append(exposedPorts, port)
401			}
402		}
403	}
404
405	if len(exposedPorts) == 0 {
406		return "", 0, fmt.Errorf("no exposed ports found for container %s", containerID)
407	}
408
409	// Sort ports and select the lowest one (Traefik strategy)
410	var selectedPort int
411	if len(exposedPorts) == 1 {
412		selectedPort = exposedPorts[0]
413	} else {
414		// Find the lowest port number
415		selectedPort = exposedPorts[0]
416		for _, port := range exposedPorts {
417			if port < selectedPort {
418				selectedPort = port
419			}
420		}
421	}
422
423	log.Info().
424		Str("container_id", containerID).
425		Str("ip", containerIP).
426		Int("selected_port", selectedPort).
427		Ints("available_ports", exposedPorts).
428		Msg("Podman container network info detected")
429
430	return containerIP, selectedPort, nil
431}

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