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}