2.0.1 dcf7d6b

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

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