feat(docker): add docker engine integration

Lazy Docker Engine client on the local socket listing running containers with a per-call timeout; unit tests and e2e tests against a fake Engine socket and real containers.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent 88d7d6d2b9
commit 9a85e9acc8
4 files changed
+569

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
package main
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
)
// dockerHost is the Docker Engine endpoint used in production
const dockerHost = "unix:///var/run/docker.sock"
// dockerListTimeout bounds every container-list call (the SDK has no default timeout)
const dockerListTimeout = 10 * time.Second
// ContainerLister is the narrow Docker dependency of the HTTP layer
type ContainerLister interface {
ListRunningContainers(ctx context.Context) ([]container.Summary, error)
}
// DockerClient wraps the Docker Engine SDK client
type DockerClient struct {
cli *client.Client
log *slog.Logger
timeout time.Duration
}
// NewDockerClient creates a client for the given Engine host (e.g. unix:///var/run/docker.sock)
// The connection is lazy: an absent or inaccessible socket only fails individual requests
func NewDockerClient(host string, log *slog.Logger) (*DockerClient, error) {
cli, err := client.New(client.WithHost(host))
if err != nil {
return nil, fmt.Errorf("create docker client: %w", err)
}
return &DockerClient{cli: cli, log: log, timeout: dockerListTimeout}, nil
}
// ListRunningContainers returns the running containers, like `docker ps`
// The result is never nil, so it serializes as [] when empty
func (d *DockerClient) ListRunningContainers(ctx context.Context) ([]container.Summary, error) {
defer traceFunc(ctx, d.log, "DockerClient.ListRunningContainers")()
ctx, cancel := context.WithTimeout(ctx, d.timeout)
defer cancel()
// All=false: running containers only
res, err := d.cli.ContainerList(ctx, client.ContainerListOptions{})
if err != nil {
return nil, fmt.Errorf("list containers: %w", err)
}
items := res.Items
if items == nil {
items = []container.Summary{}
}
for _, c := range items {
trace(ctx, d.log, "container listed", "id", c.ID, "names", c.Names, "image", c.Image, "state", c.State)
}
return items, nil
}
// Close releases the client's resources
func (d *DockerClient) Close() error {
return d.cli.Close()
}