Files
kandev/apps/backend/internal/agent/docker/storage.go
T
João Salavisa 767cc89e6c fix(backend): move Docker client to the maintained moby client module (#1935)
github.com/docker/docker is frozen at v28.5.2 — moby publishes v29+ under
github.com/moby/moby/v2, with the API client split out into the standalone
github.com/moby/moby/client and github.com/moby/moby/api modules. That leaves
five Dependabot alerts on the old module path with no reachable fix version:

  CVE-2026-42306  high    docker cp bind mount redirection      <= 28.5.2
  CVE-2026-41567  high    PUT /containers/{id}/archive          <= 28.5.2
  CVE-2026-41568  medium  docker cp symlink swap                <= 28.5.2
  CVE-2026-34040  high    AuthZ plugin bypass                   < 29.3.1
  CVE-2026-33997  medium  plugin privilege off-by-one           < 29.3.1

All five are daemon-side defects, and we only ever imported the client. Moving
to the split client module drops the daemon source out of the module graph
entirely, so these alerts resolve and future daemon advisories will not flag us
at all. It also trims six transitive dependencies.

The new client takes an options struct per call and returns a result struct, so
this is a mechanical rewrite of the wrapper. Behavioural notes:

- Ports moved from docker/go-connections/nat to moby/api/types/network, where
  Port is opaque and PortBinding.HostIP is a netip.Addr. buildDockerPortBindings
  now returns an error for out-of-range ports and unparseable host IPs instead
  of handing the daemon a malformed request. An empty HostIP still maps to the
  zero address, preserving "publish on all interfaces".
- DiskUsage needs Verbose to get per-image and per-container records; the
  aggregate LayersSize is now Images.TotalSize.
- BuildCachePrune no longer takes KeepStorage — the client derives the legacy
  keep-storage parameter from ReservedSpace for daemons on API <= 1.47.
- ImagesPrune is now ImagePrune.
- Container state and health are typed string enums, and InspectResponse.Config
  and .State are pointers, so inspect mapping guards for nil.

Also corrects the RUSTSEC-2024-0429 note in the cargo-audit ignore list. It
claimed the advisory resolves once Tauri moves to webkit2gtk-4.1, but Tauri v2
is already on those bindings and still resolves glib to 0.18.5; the real
blocker is that gtk3-rs is archived. Backport submitted upstream.

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-24 23:33:41 +01:00

134 lines
3.9 KiB
Go

package docker
import (
"context"
"fmt"
"math"
"strconv"
"time"
"github.com/moby/moby/client"
)
type storageAPI interface {
DiskUsage(context.Context, client.DiskUsageOptions) (client.DiskUsageResult, error)
BuildCachePrune(context.Context, client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
ImagePrune(context.Context, client.ImagePruneOptions) (client.ImagePruneResult, error)
}
type ImageUsage struct {
ID string `json:"id"`
SizeBytes int64 `json:"size_bytes"`
Containers int64 `json:"containers"`
CreatedAt time.Time `json:"created_at"`
}
type ContainerUsage struct {
ID string `json:"id"`
WritableBytes int64 `json:"writable_bytes"`
Labels map[string]string `json:"labels"`
}
type DiskUsage struct {
ImageLayerBytes int64 `json:"image_layer_bytes"`
BuildCacheBytes int64 `json:"build_cache_bytes"`
Images []ImageUsage `json:"images"`
Containers []ContainerUsage `json:"containers"`
}
type BuildCachePruneOptions struct {
KeepBytes int64
UnusedBefore time.Time
}
type PruneResult struct {
Deleted int `json:"deleted"`
BytesReclaimed int64 `json:"bytes_reclaimed"`
}
func (c *Client) DiskUsage(ctx context.Context) (DiskUsage, error) {
// Verbose is required for the daemon to return per-image and per-container
// records alongside the aggregate totals.
usage, err := c.storageClient().DiskUsage(ctx, client.DiskUsageOptions{
Containers: true,
Images: true,
BuildCache: true,
Verbose: true,
})
if err != nil {
return DiskUsage{}, fmt.Errorf("read Docker disk usage: %w", err)
}
result := DiskUsage{
ImageLayerBytes: usage.Images.TotalSize,
Images: make([]ImageUsage, 0, len(usage.Images.Items)),
Containers: make([]ContainerUsage, 0, len(usage.Containers.Items)),
}
for _, cache := range usage.BuildCache.Items {
if cache.Size > 0 {
result.BuildCacheBytes += cache.Size
}
}
for _, item := range usage.Images.Items {
result.Images = append(result.Images, ImageUsage{
ID: item.ID, SizeBytes: item.Size, Containers: item.Containers,
CreatedAt: time.Unix(item.Created, 0).UTC(),
})
}
for _, item := range usage.Containers.Items {
result.Containers = append(result.Containers, ContainerUsage{
ID: item.ID, WritableBytes: item.SizeRw, Labels: item.Labels,
})
}
return result, nil
}
func (c *Client) PruneBuildCache(ctx context.Context, options BuildCachePruneOptions) (PruneResult, error) {
// The client translates ReservedSpace into the legacy "keep-storage"
// parameter when talking to daemons on API <= 1.47.
result, err := c.storageClient().BuildCachePrune(ctx, client.BuildCachePruneOptions{
All: true,
ReservedSpace: options.KeepBytes,
Filters: make(client.Filters).Add("until", unixFilter(options.UnusedBefore)),
})
if err != nil {
return PruneResult{}, fmt.Errorf("prune Docker build cache: %w", err)
}
return PruneResult{
Deleted: len(result.Report.CachesDeleted),
BytesReclaimed: reclaimedBytes(result.Report.SpaceReclaimed),
}, nil
}
func (c *Client) PruneUnusedImages(ctx context.Context, unusedBefore time.Time) (PruneResult, error) {
result, err := c.storageClient().ImagePrune(ctx, client.ImagePruneOptions{
Filters: make(client.Filters).
Add("dangling", "false").
Add("until", unixFilter(unusedBefore)),
})
if err != nil {
return PruneResult{}, fmt.Errorf("prune unused Docker images: %w", err)
}
return PruneResult{
Deleted: len(result.Report.ImagesDeleted),
BytesReclaimed: reclaimedBytes(result.Report.SpaceReclaimed),
}, nil
}
func (c *Client) storageClient() storageAPI {
if c.storage != nil {
return c.storage
}
return c.cli
}
func unixFilter(value time.Time) string {
return strconv.FormatInt(value.UTC().Unix(), 10)
}
func reclaimedBytes(value uint64) int64 {
if value > math.MaxInt64 {
return math.MaxInt64
}
return int64(value)
}