v2.2.3 133ff88

runtime.go RawHistoryFolder

1// Package docker implements the container runtime adapter using Docker API.
2package docker
3
4import (
5	"context"
6	"encoding/base64"
7	"encoding/json"
8	"fmt"
9	"io"
10	"strconv"
11	"strings"
12
13	"github.com/bnema/zerowrap"
14	cerrdefs "github.com/containerd/errdefs"
15	"github.com/docker/docker/api/types/container"
16	"github.com/docker/docker/api/types/image"
17	"github.com/docker/docker/api/types/network"
18	"github.com/docker/docker/api/types/registry"
19	"github.com/docker/docker/api/types/volume"
20	"github.com/docker/docker/client"
21	"github.com/docker/go-connections/nat"
22
23	"gordon/internal/domain"
24)
25
26// Runtime implements the ContainerRuntime interface using Docker API.
27type Runtime struct {
28	client *client.Client
29}
30
31// NewRuntime creates a new Docker runtime instance.
32func NewRuntime() (*Runtime, error) {
33	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
34	if err != nil {
35		return nil, fmt.Errorf("failed to create Docker client: %w", err)
36	}
37
38	return &Runtime{
39		client: cli,
40	}, nil
41}
42
43// NewRuntimeWithClient creates a new Docker runtime instance with a custom client (for testing).
44func NewRuntimeWithClient(cli *client.Client) *Runtime {
45	return &Runtime{
46		client: cli,
47	}
48}
49
50// CreateContainer creates a new container.
51func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerConfig) (*domain.Container, error) {
52	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
53		zerowrap.FieldLayer:   "adapter",
54		zerowrap.FieldAdapter: "docker",
55		zerowrap.FieldAction:  "CreateContainer",
56		"container_name":      config.Name,
57		"image":               config.Image,
58	})
59	log := zerowrap.FromCtx(ctx)
60
61	// Convert ports to Docker format
62	exposedPorts := make(nat.PortSet)
63	portBindings := make(nat.PortMap)
64
65	for _, port := range config.Ports {
66		containerPort := nat.Port(fmt.Sprintf("%d/tcp", port))
67		exposedPorts[containerPort] = struct{}{}
68
69		// Bind to random available port on host
70		portBindings[containerPort] = []nat.PortBinding{
71			{
72				HostIP:   "0.0.0.0",
73				HostPort: "0", // Docker will assign a random available port
74			},
75		}
76	}
77
78	// Convert volumes to Docker format
79	var binds []string
80	if config.Volumes != nil {
81		for containerPath, volumeName := range config.Volumes {
82			bind := fmt.Sprintf("%s:%s", volumeName, containerPath)
83			binds = append(binds, bind)
84			log.Debug().Str("volume", volumeName).Str("mount_path", containerPath).Msg("adding volume mount")
85		}
86	}
87
88	// Create container configuration
89	containerConfig := &container.Config{
90		Image:        config.Image,
91		Hostname:     config.Hostname,
92		Env:          config.Env,
93		ExposedPorts: exposedPorts,
94		WorkingDir:   config.WorkingDir,
95		Cmd:          config.Cmd,
96		Labels:       config.Labels,
97	}
98
99	hostConfig := &container.HostConfig{
100		PortBindings: portBindings,
101		AutoRemove:   config.AutoRemove,
102		Binds:        binds,
103		NetworkMode:  container.NetworkMode(config.NetworkMode),
104	}
105
106	// Create network configuration for container
107	var networkConfig *network.NetworkingConfig
108	if config.NetworkMode != "" && config.NetworkMode != "default" && config.NetworkMode != "bridge" {
109		networkConfig = &network.NetworkingConfig{
110			EndpointsConfig: map[string]*network.EndpointSettings{
111				config.NetworkMode: {
112					Aliases: config.Aliases,
113				},
114			},
115		}
116		if config.Hostname != "" {
117			networkConfig.EndpointsConfig[config.NetworkMode].NetworkID = config.NetworkMode
118		}
119	}
120
121	// Create the container
122	resp, err := r.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, config.Name)
123	if err != nil {
124		return nil, log.WrapErr(err, "failed to create container")
125	}
126
127	log.Info().Str(zerowrap.FieldEntityID, resp.ID).Msg("container created")
128
129	// Inspect the created container to get full details
130	return r.InspectContainer(ctx, resp.ID)
131}
132
133// StartContainer starts a container.
134func (r *Runtime) StartContainer(ctx context.Context, containerID string) error {
135	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
136		zerowrap.FieldLayer:    "adapter",
137		zerowrap.FieldAdapter:  "docker",
138		zerowrap.FieldAction:   "StartContainer",
139		zerowrap.FieldEntityID: containerID,
140	})
141	log := zerowrap.FromCtx(ctx)
142
143	err := r.client.ContainerStart(ctx, containerID, container.StartOptions{})
144	if err != nil {
145		return log.WrapErr(err, "failed to start container")
146	}
147
148	log.Info().Msg("container started")
149	return nil
150}
151
152// StopContainer stops a container.
153func (r *Runtime) StopContainer(ctx context.Context, containerID string) error {
154	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
155		zerowrap.FieldLayer:    "adapter",
156		zerowrap.FieldAdapter:  "docker",
157		zerowrap.FieldAction:   "StopContainer",
158		zerowrap.FieldEntityID: containerID,
159	})
160	log := zerowrap.FromCtx(ctx)
161
162	timeout := 30 // 30 seconds timeout
163	err := r.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
164	if err != nil {
165		return log.WrapErr(err, "failed to stop container")
166	}
167
168	log.Info().Msg("container stopped")
169	return nil
170}
171
172// RestartContainer restarts a container.
173func (r *Runtime) RestartContainer(ctx context.Context, containerID string) error {
174	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
175		zerowrap.FieldLayer:    "adapter",
176		zerowrap.FieldAdapter:  "docker",
177		zerowrap.FieldAction:   "RestartContainer",
178		zerowrap.FieldEntityID: containerID,
179	})
180	log := zerowrap.FromCtx(ctx)
181
182	timeout := 30 // 30 seconds timeout
183	err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{Timeout: &timeout})
184	if err != nil {
185		return log.WrapErr(err, "failed to restart container")
186	}
187
188	log.Info().Msg("container restarted")
189	return nil
190}
191
192// RemoveContainer removes a container.
193func (r *Runtime) RemoveContainer(ctx context.Context, containerID string, force bool) error {
194	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
195		zerowrap.FieldLayer:    "adapter",
196		zerowrap.FieldAdapter:  "docker",
197		zerowrap.FieldAction:   "RemoveContainer",
198		zerowrap.FieldEntityID: containerID,
199		"force":                force,
200	})
201	log := zerowrap.FromCtx(ctx)
202
203	err := r.client.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: force})
204	if err != nil {
205		return log.WrapErr(err, "failed to remove container")
206	}
207
208	log.Info().Msg("container removed")
209	return nil
210}
211
212// RenameContainer renames a container.
213func (r *Runtime) RenameContainer(ctx context.Context, containerID, newName string) error {
214	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
215		zerowrap.FieldLayer:    "adapter",
216		zerowrap.FieldAdapter:  "docker",
217		zerowrap.FieldAction:   "RenameContainer",
218		zerowrap.FieldEntityID: containerID,
219		"new_name":             newName,
220	})
221	log := zerowrap.FromCtx(ctx)
222
223	err := r.client.ContainerRename(ctx, containerID, newName)
224	if err != nil {
225		return log.WrapErr(err, "failed to rename container")
226	}
227
228	log.Info().Msg("container renamed")
229	return nil
230}
231
232// ListContainers lists containers.
233func (r *Runtime) ListContainers(ctx context.Context, all bool) ([]*domain.Container, error) {
234	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
235		zerowrap.FieldLayer:   "adapter",
236		zerowrap.FieldAdapter: "docker",
237		zerowrap.FieldAction:  "ListContainers",
238		"all":                 all,
239	})
240	log := zerowrap.FromCtx(ctx)
241
242	containers, err := r.client.ContainerList(ctx, container.ListOptions{All: all})
243	if err != nil {
244		return nil, log.WrapErr(err, "failed to list containers")
245	}
246
247	var result []*domain.Container
248	for _, c := range containers {
249		// Extract ports
250		var ports []int
251		for _, port := range c.Ports {
252			if port.PublicPort > 0 {
253				ports = append(ports, int(port.PublicPort))
254			}
255		}
256
257		// Get the primary name (remove leading slash)
258		name := ""
259		if len(c.Names) > 0 {
260			name = strings.TrimPrefix(c.Names[0], "/")
261		}
262
263		result = append(result, &domain.Container{
264			ID:     c.ID,
265			Image:  c.Image,
266			Name:   name,
267			Status: c.Status,
268			Ports:  ports,
269			Labels: c.Labels,
270		})
271	}
272
273	return result, nil
274}
275
276// InspectContainer inspects a container.
277func (r *Runtime) InspectContainer(ctx context.Context, containerID string) (*domain.Container, error) {
278	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
279		zerowrap.FieldLayer:    "adapter",
280		zerowrap.FieldAdapter:  "docker",
281		zerowrap.FieldAction:   "InspectContainer",
282		zerowrap.FieldEntityID: containerID,
283	})
284	log := zerowrap.FromCtx(ctx)
285
286	resp, err := r.client.ContainerInspect(ctx, containerID)
287	if err != nil {
288		return nil, log.WrapErr(err, "failed to inspect container")
289	}
290
291	// Extract published ports
292	var ports []int
293	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
294		for _, bindings := range resp.NetworkSettings.Ports {
295			for _, binding := range bindings {
296				if binding.HostPort != "" {
297					if port, err := strconv.Atoi(binding.HostPort); err == nil {
298						ports = append(ports, port)
299					}
300				}
301			}
302		}
303	}
304
305	// Get container name (remove leading slash)
306	name := strings.TrimPrefix(resp.Name, "/")
307
308	return &domain.Container{
309		ID:     resp.ID,
310		Image:  resp.Config.Image,
311		Name:   name,
312		Status: resp.State.Status,
313		Ports:  ports,
314		Labels: resp.Config.Labels,
315	}, nil
316}
317
318// GetContainerLogs gets container logs.
319func (r *Runtime) GetContainerLogs(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
320	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
321		zerowrap.FieldLayer:    "adapter",
322		zerowrap.FieldAdapter:  "docker",
323		zerowrap.FieldAction:   "GetContainerLogs",
324		zerowrap.FieldEntityID: containerID,
325	})
326	log := zerowrap.FromCtx(ctx)
327
328	logs, err := r.client.ContainerLogs(ctx, containerID, container.LogsOptions{
329		ShowStdout: true,
330		ShowStderr: true,
331		Follow:     follow,
332		Timestamps: true,
333	})
334	if err != nil {
335		return nil, log.WrapErr(err, "failed to get container logs")
336	}
337
338	return logs, nil
339}
340
341// PullImage pulls an image.
342func (r *Runtime) PullImage(ctx context.Context, imageRef string) error {
343	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
344		zerowrap.FieldLayer:   "adapter",
345		zerowrap.FieldAdapter: "docker",
346		zerowrap.FieldAction:  "PullImage",
347		"image":               imageRef,
348	})
349	log := zerowrap.FromCtx(ctx)
350
351	log.Info().Msg("pulling image")
352
353	reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{})
354	if err != nil {
355		return log.WrapErr(err, "failed to pull image")
356	}
357	defer reader.Close()
358
359	// Read the response to completion (this is required for the pull to complete)
360	_, err = io.Copy(io.Discard, reader)
361	if err != nil {
362		return log.WrapErr(err, "failed to read pull response")
363	}
364
365	log.Info().Msg("image pulled successfully")
366	return nil
367}
368
369// PullImageWithAuth pulls an image with authentication.
370func (r *Runtime) PullImageWithAuth(ctx context.Context, imageRef, username, password string) error {
371	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
372		zerowrap.FieldLayer:   "adapter",
373		zerowrap.FieldAdapter: "docker",
374		zerowrap.FieldAction:  "PullImageWithAuth",
375		"image":               imageRef,
376		"username":            username,
377	})
378	log := zerowrap.FromCtx(ctx)
379
380	log.Info().Msg("pulling image with authentication")
381
382	// Extract registry server address from image reference
383	// e.g., "registry.bnema.dev/image:tag" -> "registry.bnema.dev"
384	serverAddress := imageRef
385	if idx := strings.Index(imageRef, "/"); idx > 0 {
386		serverAddress = imageRef[:idx]
387	}
388
389	// Create authentication configuration
390	authConfig := registry.AuthConfig{
391		Username:      username,
392		Password:      password,
393		ServerAddress: serverAddress,
394	}
395
396	// Encode authentication to base64 JSON
397	// Note: Docker API accepts both StdEncoding and URLEncoding, but Podman
398	// may be more strict. Using StdEncoding for maximum compatibility.
399	authConfigBytes, err := json.Marshal(authConfig)
400	if err != nil {
401		return log.WrapErr(err, "failed to marshal auth config")
402	}
403	authStr := base64.StdEncoding.EncodeToString(authConfigBytes)
404
405	log.Debug().
406		Str("server_address", serverAddress).
407		Str("auth_json", string(authConfigBytes)).
408		Msg("auth config for pull")
409
410	// Pull with authentication
411	reader, err := r.client.ImagePull(ctx, imageRef, image.PullOptions{
412		RegistryAuth: authStr,
413	})
414	if err != nil {
415		return log.WrapErr(err, "failed to pull image with auth")
416	}
417	defer reader.Close()
418
419	// Read the response to completion (this is required for the pull to complete)
420	_, err = io.Copy(io.Discard, reader)
421	if err != nil {
422		return log.WrapErr(err, "failed to read pull response")
423	}
424
425	log.Info().Msg("image pulled successfully with authentication")
426	return nil
427}
428
429// TagImage tags a locally available image with a new reference.
430func (r *Runtime) TagImage(ctx context.Context, sourceRef, targetRef string) error {
431	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
432		zerowrap.FieldLayer:   "adapter",
433		zerowrap.FieldAdapter: "docker",
434		zerowrap.FieldAction:  "TagImage",
435		"source":              sourceRef,
436		"target":              targetRef,
437	})
438	log := zerowrap.FromCtx(ctx)
439
440	if err := r.client.ImageTag(ctx, sourceRef, targetRef); err != nil {
441		return log.WrapErr(err, "failed to tag image")
442	}
443
444	log.Info().Msg("image tagged successfully")
445	return nil
446}
447
448// UntagImage removes a tag from an image without deleting the underlying image layers.
449func (r *Runtime) UntagImage(ctx context.Context, imageRef string) error {
450	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
451		zerowrap.FieldLayer:   "adapter",
452		zerowrap.FieldAdapter: "docker",
453		zerowrap.FieldAction:  "UntagImage",
454		"image":               imageRef,
455	})
456	log := zerowrap.FromCtx(ctx)
457
458	// ImageRemove with PruneChildren=false only removes the tag, not the image layers
459	_, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{
460		Force:         false,
461		PruneChildren: false,
462	})
463	if err != nil {
464		return log.WrapErr(err, "failed to untag image")
465	}
466
467	log.Debug().Msg("image untagged successfully")
468	return nil
469}
470
471// RemoveImage removes an image.
472func (r *Runtime) RemoveImage(ctx context.Context, imageRef string, force bool) error {
473	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
474		zerowrap.FieldLayer:   "adapter",
475		zerowrap.FieldAdapter: "docker",
476		zerowrap.FieldAction:  "RemoveImage",
477		"image":               imageRef,
478		"force":               force,
479	})
480	log := zerowrap.FromCtx(ctx)
481
482	_, err := r.client.ImageRemove(ctx, imageRef, image.RemoveOptions{Force: force})
483	if err != nil {
484		return log.WrapErr(err, "failed to remove image")
485	}
486
487	log.Info().Msg("image removed")
488	return nil
489}
490
491// ListImages lists images.
492func (r *Runtime) ListImages(ctx context.Context) ([]string, error) {
493	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
494		zerowrap.FieldLayer:   "adapter",
495		zerowrap.FieldAdapter: "docker",
496		zerowrap.FieldAction:  "ListImages",
497	})
498	log := zerowrap.FromCtx(ctx)
499
500	images, err := r.client.ImageList(ctx, image.ListOptions{})
501	if err != nil {
502		return nil, log.WrapErr(err, "failed to list images")
503	}
504
505	var result []string
506	for _, img := range images {
507		for _, tag := range img.RepoTags {
508			if tag != "<none>:<none>" {
509				result = append(result, tag)
510			}
511		}
512	}
513
514	return result, nil
515}
516
517// Ping checks if Docker is responsive.
518func (r *Runtime) Ping(ctx context.Context) error {
519	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
520		zerowrap.FieldLayer:   "adapter",
521		zerowrap.FieldAdapter: "docker",
522		zerowrap.FieldAction:  "Ping",
523	})
524	log := zerowrap.FromCtx(ctx)
525
526	_, err := r.client.Ping(ctx)
527	if err != nil {
528		return log.WrapErr(err, "Docker ping failed")
529	}
530	return nil
531}
532
533// Version returns Docker version.
534func (r *Runtime) Version(ctx context.Context) (string, error) {
535	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
536		zerowrap.FieldLayer:   "adapter",
537		zerowrap.FieldAdapter: "docker",
538		zerowrap.FieldAction:  "Version",
539	})
540	log := zerowrap.FromCtx(ctx)
541
542	version, err := r.client.ServerVersion(ctx)
543	if err != nil {
544		return "", log.WrapErr(err, "failed to get Docker version")
545	}
546	return version.Version, nil
547}
548
549// IsContainerRunning checks if a container is running.
550func (r *Runtime) IsContainerRunning(ctx context.Context, containerID string) (bool, error) {
551	ctr, err := r.InspectContainer(ctx, containerID)
552	if err != nil {
553		return false, err
554	}
555	return ctr.Status == string(domain.ContainerStatusRunning), nil
556}
557
558// GetContainerPort gets the host port for a container's internal port.
559func (r *Runtime) GetContainerPort(ctx context.Context, containerID string, internalPort int) (int, error) {
560	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
561		zerowrap.FieldLayer:    "adapter",
562		zerowrap.FieldAdapter:  "docker",
563		zerowrap.FieldAction:   "GetContainerPort",
564		zerowrap.FieldEntityID: containerID,
565		"internal_port":        internalPort,
566	})
567	log := zerowrap.FromCtx(ctx)
568
569	resp, err := r.client.ContainerInspect(ctx, containerID)
570	if err != nil {
571		return 0, log.WrapErr(err, "failed to inspect container")
572	}
573
574	if resp.NetworkSettings == nil || resp.NetworkSettings.Ports == nil {
575		return 0, fmt.Errorf("no port mappings found for container %s", containerID)
576	}
577
578	containerPort := nat.Port(fmt.Sprintf("%d/tcp", internalPort))
579	bindings, exists := resp.NetworkSettings.Ports[containerPort]
580	if !exists || len(bindings) == 0 {
581		return 0, fmt.Errorf("port %d not mapped for container %s", internalPort, containerID)
582	}
583
584	hostPort, err := strconv.Atoi(bindings[0].HostPort)
585	if err != nil {
586		return 0, fmt.Errorf("invalid host port for container %s: %w", containerID, err)
587	}
588
589	return hostPort, nil
590}
591
592// GetImageExposedPorts gets the exposed ports from an image.
593func (r *Runtime) GetImageExposedPorts(ctx context.Context, imageRef string) ([]int, error) {
594	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
595		zerowrap.FieldLayer:   "adapter",
596		zerowrap.FieldAdapter: "docker",
597		zerowrap.FieldAction:  "GetImageExposedPorts",
598		"image":               imageRef,
599	})
600	log := zerowrap.FromCtx(ctx)
601
602	imageInspect, err := r.client.ImageInspect(ctx, imageRef)
603	if err != nil {
604		return nil, log.WrapErr(err, "failed to inspect image")
605	}
606
607	var ports []int
608	if imageInspect.Config != nil && imageInspect.Config.ExposedPorts != nil {
609		for portSpec := range imageInspect.Config.ExposedPorts {
610			// Parse port from format like "80/tcp"
611			portStr := strings.Split(portSpec, "/")[0]
612			if port, err := strconv.Atoi(portStr); err == nil {
613				ports = append(ports, port)
614			}
615		}
616	}
617
618	if len(ports) == 0 {
619		return nil, fmt.Errorf("no EXPOSE directives found in image %s - please add EXPOSE directive to Dockerfile", imageRef)
620	}
621
622	log.Info().Ints("exposed_ports", ports).Msg("found exposed ports in image")
623	return ports, nil
624}
625
626// GetContainerExposedPorts gets all exposed ports from a running container.
627func (r *Runtime) GetContainerExposedPorts(ctx context.Context, containerID string) ([]int, error) {
628	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
629		zerowrap.FieldLayer:    "adapter",
630		zerowrap.FieldAdapter:  "docker",
631		zerowrap.FieldAction:   "GetContainerExposedPorts",
632		zerowrap.FieldEntityID: containerID,
633	})
634	log := zerowrap.FromCtx(ctx)
635
636	resp, err := r.client.ContainerInspect(ctx, containerID)
637	if err != nil {
638		return nil, log.WrapErr(err, "failed to inspect container")
639	}
640
641	var ports []int
642	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
643		for portSpec := range resp.NetworkSettings.Ports {
644			// Parse port from format like "80/tcp"
645			portStr := strings.Split(string(portSpec), "/")[0]
646			if port, err := strconv.Atoi(portStr); err == nil {
647				ports = append(ports, port)
648			}
649		}
650	}
651
652	return ports, nil
653}
654
655// GetContainerNetworkInfo gets container's internal IP and automatically detects the best port to use.
656func (r *Runtime) GetContainerNetworkInfo(ctx context.Context, containerID string) (string, int, error) {
657	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
658		zerowrap.FieldLayer:    "adapter",
659		zerowrap.FieldAdapter:  "docker",
660		zerowrap.FieldAction:   "GetContainerNetworkInfo",
661		zerowrap.FieldEntityID: containerID,
662	})
663	log := zerowrap.FromCtx(ctx)
664
665	resp, err := r.client.ContainerInspect(ctx, containerID)
666	if err != nil {
667		return "", 0, log.WrapErr(err, "failed to inspect container")
668	}
669
670	// Get container's internal IP address
671	var containerIP string
672	if resp.NetworkSettings != nil && resp.NetworkSettings.Networks != nil {
673		// Use the first available network (usually bridge or custom network)
674		for _, net := range resp.NetworkSettings.Networks {
675			if net.IPAddress != "" {
676				containerIP = net.IPAddress
677				break
678			}
679		}
680	}
681
682	if containerIP == "" {
683		return "", 0, fmt.Errorf("no IP address found for container %s", containerID)
684	}
685
686	// Get exposed ports from container
687	var exposedPorts []int
688	if resp.NetworkSettings != nil && resp.NetworkSettings.Ports != nil {
689		for portSpec := range resp.NetworkSettings.Ports {
690			// Parse port from format like "80/tcp"
691			portStr := strings.Split(string(portSpec), "/")[0]
692			if port, err := strconv.Atoi(portStr); err == nil {
693				exposedPorts = append(exposedPorts, port)
694			}
695		}
696	}
697
698	if len(exposedPorts) == 0 {
699		return "", 0, fmt.Errorf("no EXPOSE directives found for container %s - please add EXPOSE directive to Dockerfile", containerID)
700	}
701
702	// Use the first exposed port
703	selectedPort := exposedPorts[0]
704
705	log.Info().
706		Str("ip", containerIP).
707		Int("selected_port", selectedPort).
708		Ints("exposed_ports", exposedPorts).
709		Msg("container network info detected")
710
711	return containerIP, selectedPort, nil
712}
713
714// InspectImageVolumes gets the volume mount points declared in the image.
715func (r *Runtime) InspectImageVolumes(ctx context.Context, imageRef string) ([]string, error) {
716	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
717		zerowrap.FieldLayer:   "adapter",
718		zerowrap.FieldAdapter: "docker",
719		zerowrap.FieldAction:  "InspectImageVolumes",
720		"image":               imageRef,
721	})
722	log := zerowrap.FromCtx(ctx)
723
724	imageInspect, err := r.client.ImageInspect(ctx, imageRef)
725	if err != nil {
726		return nil, log.WrapErr(err, "failed to inspect image")
727	}
728
729	var volumes []string
730	if imageInspect.Config != nil && imageInspect.Config.Volumes != nil {
731		for volumePath := range imageInspect.Config.Volumes {
732			volumes = append(volumes, volumePath)
733		}
734	}
735
736	if len(volumes) > 0 {
737		log.Info().Strs("volumes", volumes).Msg("found VOLUME directives in image")
738	} else {
739		log.Debug().Msg("no VOLUME directives found in image")
740	}
741
742	return volumes, nil
743}
744
745// VolumeExists checks if a Docker volume exists.
746func (r *Runtime) VolumeExists(ctx context.Context, volumeName string) (bool, error) {
747	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
748		zerowrap.FieldLayer:   "adapter",
749		zerowrap.FieldAdapter: "docker",
750		zerowrap.FieldAction:  "VolumeExists",
751		"volume":              volumeName,
752	})
753	log := zerowrap.FromCtx(ctx)
754
755	_, err := r.client.VolumeInspect(ctx, volumeName)
756	if err != nil {
757		if cerrdefs.IsNotFound(err) {
758			return false, nil
759		}
760		return false, log.WrapErr(err, "failed to inspect volume")
761	}
762	return true, nil
763}
764
765// CreateVolume creates a new Docker volume.
766func (r *Runtime) CreateVolume(ctx context.Context, volumeName string) error {
767	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
768		zerowrap.FieldLayer:   "adapter",
769		zerowrap.FieldAdapter: "docker",
770		zerowrap.FieldAction:  "CreateVolume",
771		"volume":              volumeName,
772	})
773	log := zerowrap.FromCtx(ctx)
774
775	_, err := r.client.VolumeCreate(ctx, volume.CreateOptions{
776		Name: volumeName,
777		Labels: map[string]string{
778			"gordon.managed": "true",
779			"gordon.created": "auto",
780		},
781	})
782	if err != nil {
783		return log.WrapErr(err, "failed to create volume")
784	}
785
786	log.Info().Msg("volume created")
787	return nil
788}
789
790// RemoveVolume removes a Docker volume.
791func (r *Runtime) RemoveVolume(ctx context.Context, volumeName string, force bool) error {
792	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
793		zerowrap.FieldLayer:   "adapter",
794		zerowrap.FieldAdapter: "docker",
795		zerowrap.FieldAction:  "RemoveVolume",
796		"volume":              volumeName,
797		"force":               force,
798	})
799	log := zerowrap.FromCtx(ctx)
800
801	err := r.client.VolumeRemove(ctx, volumeName, force)
802	if err != nil {
803		if cerrdefs.IsNotFound(err) {
804			log.Debug().Msg("volume not found, already removed")
805			return nil
806		}
807		return log.WrapErr(err, "failed to remove volume")
808	}
809
810	log.Info().Msg("volume removed")
811	return nil
812}
813
814// InspectImageEnv gets the environment variables declared in the image.
815func (r *Runtime) InspectImageEnv(ctx context.Context, imageRef string) ([]string, error) {
816	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
817		zerowrap.FieldLayer:   "adapter",
818		zerowrap.FieldAdapter: "docker",
819		zerowrap.FieldAction:  "InspectImageEnv",
820		"image":               imageRef,
821	})
822	log := zerowrap.FromCtx(ctx)
823
824	imageInspect, err := r.client.ImageInspect(ctx, imageRef)
825	if err != nil {
826		return nil, log.WrapErr(err, "failed to inspect image")
827	}
828
829	var envVars []string
830	if imageInspect.Config != nil && imageInspect.Config.Env != nil {
831		envVars = imageInspect.Config.Env
832	}
833
834	if len(envVars) > 0 {
835		log.Debug().Strs("env_vars", envVars).Msg("found ENV directives in image")
836	} else {
837		log.Debug().Msg("no ENV directives found in image")
838	}
839
840	return envVars, nil
841}
842
843// GetImageLabels returns the labels defined on an image.
844func (r *Runtime) GetImageLabels(ctx context.Context, imageRef string) (map[string]string, error) {
845	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
846		zerowrap.FieldLayer:   "adapter",
847		zerowrap.FieldAdapter: "docker",
848		zerowrap.FieldAction:  "GetImageLabels",
849		"image":               imageRef,
850	})
851	log := zerowrap.FromCtx(ctx)
852
853	imageInspect, err := r.client.ImageInspect(ctx, imageRef)
854	if err != nil {
855		return nil, log.WrapErr(err, "failed to inspect image")
856	}
857
858	labels := make(map[string]string)
859	if imageInspect.Config != nil && imageInspect.Config.Labels != nil {
860		labels = imageInspect.Config.Labels
861	}
862
863	log.Debug().Int("label_count", len(labels)).Msg("retrieved image labels")
864	return labels, nil
865}
866
867// CreateNetwork creates a new Docker network.
868func (r *Runtime) CreateNetwork(ctx context.Context, name string, options map[string]string) error {
869	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
870		zerowrap.FieldLayer:   "adapter",
871		zerowrap.FieldAdapter: "docker",
872		zerowrap.FieldAction:  "CreateNetwork",
873		"network":             name,
874	})
875	log := zerowrap.FromCtx(ctx)
876
877	// Set default driver to bridge if not specified
878	driver := "bridge"
879	if driverOption, exists := options["driver"]; exists {
880		driver = driverOption
881	}
882
883	createOptions := network.CreateOptions{
884		Driver: driver,
885		Labels: map[string]string{
886			"gordon.managed": "true",
887		},
888	}
889
890	// Add any additional options to labels
891	for key, value := range options {
892		if key != "driver" {
893			createOptions.Labels["gordon."+key] = value
894		}
895	}
896
897	_, err := r.client.NetworkCreate(ctx, name, createOptions)
898	if err != nil {
899		return log.WrapErr(err, "failed to create network")
900	}
901
902	log.Info().Str("driver", driver).Msg("network created")
903	return nil
904}
905
906// RemoveNetwork removes a Docker network.
907func (r *Runtime) RemoveNetwork(ctx context.Context, name string) error {
908	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
909		zerowrap.FieldLayer:   "adapter",
910		zerowrap.FieldAdapter: "docker",
911		zerowrap.FieldAction:  "RemoveNetwork",
912		"network":             name,
913	})
914	log := zerowrap.FromCtx(ctx)
915
916	err := r.client.NetworkRemove(ctx, name)
917	if err != nil {
918		if cerrdefs.IsNotFound(err) {
919			log.Debug().Msg("network not found, already removed")
920			return nil
921		}
922		return log.WrapErr(err, "failed to remove network")
923	}
924
925	log.Info().Msg("network removed")
926	return nil
927}
928
929// ListNetworks lists all Docker networks.
930func (r *Runtime) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) {
931	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
932		zerowrap.FieldLayer:   "adapter",
933		zerowrap.FieldAdapter: "docker",
934		zerowrap.FieldAction:  "ListNetworks",
935	})
936	log := zerowrap.FromCtx(ctx)
937
938	networks, err := r.client.NetworkList(ctx, network.ListOptions{})
939	if err != nil {
940		return nil, log.WrapErr(err, "failed to list networks")
941	}
942
943	var result []*domain.NetworkInfo
944	for _, net := range networks {
945		var containers []string
946		for containerID := range net.Containers {
947			containers = append(containers, containerID)
948		}
949
950		result = append(result, &domain.NetworkInfo{
951			ID:         net.ID,
952			Name:       net.Name,
953			Driver:     net.Driver,
954			Containers: containers,
955			Labels:     net.Labels,
956		})
957	}
958
959	return result, nil
960}
961
962// NetworkExists checks if a Docker network exists.
963func (r *Runtime) NetworkExists(ctx context.Context, name string) (bool, error) {
964	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
965		zerowrap.FieldLayer:   "adapter",
966		zerowrap.FieldAdapter: "docker",
967		zerowrap.FieldAction:  "NetworkExists",
968		"network":             name,
969	})
970	log := zerowrap.FromCtx(ctx)
971
972	_, err := r.client.NetworkInspect(ctx, name, network.InspectOptions{})
973	if err != nil {
974		if cerrdefs.IsNotFound(err) {
975			return false, nil
976		}
977		return false, log.WrapErr(err, "failed to inspect network")
978	}
979	return true, nil
980}
981
982// ConnectContainerToNetwork connects a container to a network.
983func (r *Runtime) ConnectContainerToNetwork(ctx context.Context, containerName, networkName string) error {
984	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
985		zerowrap.FieldLayer:   "adapter",
986		zerowrap.FieldAdapter: "docker",
987		zerowrap.FieldAction:  "ConnectContainerToNetwork",
988		"container":           containerName,
989		"network":             networkName,
990	})
991	log := zerowrap.FromCtx(ctx)
992
993	err := r.client.NetworkConnect(ctx, networkName, containerName, &network.EndpointSettings{})
994	if err != nil {
995		return log.WrapErr(err, "failed to connect container to network")
996	}
997
998	log.Info().Msg("container connected to network")
999	return nil
1000}
1001
1002// DisconnectContainerFromNetwork disconnects a container from a network.
1003func (r *Runtime) DisconnectContainerFromNetwork(ctx context.Context, containerName, networkName string) error {
1004	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
1005		zerowrap.FieldLayer:   "adapter",
1006		zerowrap.FieldAdapter: "docker",
1007		zerowrap.FieldAction:  "DisconnectContainerFromNetwork",
1008		"container":           containerName,
1009		"network":             networkName,
1010	})
1011	log := zerowrap.FromCtx(ctx)
1012
1013	err := r.client.NetworkDisconnect(ctx, networkName, containerName, false)
1014	if err != nil {
1015		return log.WrapErr(err, "failed to disconnect container from network")
1016	}
1017
1018	log.Info().Msg("container disconnected from network")
1019	return nil
1020}

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