/* © AngelaMos | 2026 client.go Docker API client wrapper with singleton initialization and per-call timeouts Wraps the official Docker SDK client using sync.Once so all analyzers share one connection. Every API method creates a child context with an operation-specific timeout from config/constants.go. Provides ping, info, container list/inspect, and image list/inspect/history. Key exports: Client - wraps docker/client.Client with timeout-aware methods NewClient - singleton constructor reading DOCKER_HOST from environment Connects to: config/constants.go - reads ConnectionTimeout, InspectTimeout, DefaultTimeout analyzer/container.go - ListContainers, InspectContainer analyzer/daemon.go - Info analyzer/image.go - ListImages, InspectImage scanner.go - constructs Client and passes it to runtime analyzers */ package docker import ( "context" "fmt" "sync" "github.com/CarterPerez-dev/docksec/internal/config" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" ) type Client struct { api *client.Client } var ( instance *Client once sync.Once initErr error ) func NewClient() (*Client, error) { once.Do(func() { cli, err := client.NewClientWithOpts( client.FromEnv, client.WithAPIVersionNegotiation(), ) if err != nil { initErr = fmt.Errorf("creating docker client: %w", err) return } instance = &Client{api: cli} }) if initErr != nil { return nil, initErr } return instance, nil } func (c *Client) Close() error { if c.api != nil { return c.api.Close() } return nil } func (c *Client) Ping(ctx context.Context) error { pingCtx, cancel := context.WithTimeout(ctx, config.ConnectionTimeout) defer cancel() _, err := c.api.Ping(pingCtx) if err != nil { return fmt.Errorf("pinging docker daemon: %w", err) } return nil } func (c *Client) Info(ctx context.Context) (system.Info, error) { infoCtx, cancel := context.WithTimeout(ctx, config.DefaultTimeout) defer cancel() info, err := c.api.Info(infoCtx) if err != nil { return system.Info{}, fmt.Errorf("getting docker info: %w", err) } return info, nil } func (c *Client) ServerVersion(ctx context.Context) (types.Version, error) { versionCtx, cancel := context.WithTimeout(ctx, config.ConnectionTimeout) defer cancel() version, err := c.api.ServerVersion(versionCtx) if err != nil { return types.Version{}, fmt.Errorf("getting docker version: %w", err) } return version, nil } func (c *Client) ListContainers( ctx context.Context, all bool, ) ([]container.Summary, error) { listCtx, cancel := context.WithTimeout(ctx, config.DefaultTimeout) defer cancel() containers, err := c.api.ContainerList( listCtx, container.ListOptions{All: all}, ) if err != nil { return nil, fmt.Errorf("listing containers: %w", err) } return containers, nil } func (c *Client) InspectContainer( ctx context.Context, containerID string, ) (container.InspectResponse, error) { inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout) defer cancel() info, err := c.api.ContainerInspect(inspectCtx, containerID) if err != nil { return container.InspectResponse{}, fmt.Errorf( "inspecting container %s: %w", containerID, err, ) } return info, nil } func (c *Client) ListImages(ctx context.Context) ([]image.Summary, error) { listCtx, cancel := context.WithTimeout(ctx, config.DefaultTimeout) defer cancel() images, err := c.api.ImageList(listCtx, image.ListOptions{All: false}) if err != nil { return nil, fmt.Errorf("listing images: %w", err) } return images, nil } func (c *Client) InspectImage( ctx context.Context, imageID string, ) (image.InspectResponse, error) { inspectCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout) defer cancel() info, err := c.api.ImageInspect(inspectCtx, imageID) if err != nil { return image.InspectResponse{}, fmt.Errorf( "inspecting image %s: %w", imageID, err, ) } return info, nil } func (c *Client) ImageHistory( ctx context.Context, imageID string, ) ([]image.HistoryResponseItem, error) { historyCtx, cancel := context.WithTimeout(ctx, config.InspectTimeout) defer cancel() history, err := c.api.ImageHistory(historyCtx, imageID) if err != nil { return nil, fmt.Errorf("getting image history %s: %w", imageID, err) } return history, nil }