package docker import ( "context" "fmt" "github.com/moby/moby/client" ) // Labels applied to all containers managed by Tinyforge. const ( LabelProject = "tinyforge.project" LabelStage = "tinyforge.stage" LabelInstanceID = "tinyforge.instance-id" ) // Client wraps the Docker Engine API client. type Client struct { api client.APIClient } // New creates a new Docker client connected to the default Docker socket. func New() (*Client, error) { api, err := client.NewClientWithOpts( client.FromEnv, client.WithAPIVersionNegotiation(), ) if err != nil { return nil, fmt.Errorf("create docker client: %w", err) } return &Client{api: api}, nil } // Close releases resources held by the Docker client. func (c *Client) Close() error { if err := c.api.Close(); err != nil { return fmt.Errorf("close docker client: %w", err) } return nil } // Ping checks connectivity to the Docker daemon. func (c *Client) Ping(ctx context.Context) error { _, err := c.api.Ping(ctx, client.PingOptions{}) if err != nil { return fmt.Errorf("ping docker daemon: %w", err) } return nil } // DaemonInfo captures the subset of Docker daemon info surfaced in the UI. // Fields map directly to /info and /version; JSON tags match the wire format // consumed by the frontend's DockerHealth type. type DaemonInfo struct { Version string `json:"version,omitempty"` APIVersion string `json:"api_version,omitempty"` OS string `json:"os,omitempty"` Arch string `json:"arch,omitempty"` Kernel string `json:"kernel,omitempty"` OperatingSystem string `json:"operating_system,omitempty"` StorageDriver string `json:"storage_driver,omitempty"` RootDir string `json:"root_dir,omitempty"` Name string `json:"name,omitempty"` NCPU int `json:"ncpu,omitempty"` MemoryTotal int64 `json:"memory_total,omitempty"` Containers int `json:"containers,omitempty"` Running int `json:"running,omitempty"` Paused int `json:"paused,omitempty"` Stopped int `json:"stopped,omitempty"` Images int `json:"images,omitempty"` } // Info returns a compact snapshot of daemon health data. Missing pieces // (e.g. if ServerVersion fails but Info succeeds) are returned as zero // values rather than bubbling up — the endpoint should degrade gracefully. func (c *Client) Info(ctx context.Context) (DaemonInfo, error) { info, err := c.api.Info(ctx, client.InfoOptions{}) if err != nil { return DaemonInfo{}, fmt.Errorf("docker info: %w", err) } out := DaemonInfo{ OperatingSystem: info.Info.OperatingSystem, Kernel: info.Info.KernelVersion, Arch: info.Info.Architecture, OS: info.Info.OSType, StorageDriver: info.Info.Driver, RootDir: info.Info.DockerRootDir, Name: info.Info.Name, NCPU: info.Info.NCPU, MemoryTotal: info.Info.MemTotal, Containers: info.Info.Containers, Running: info.Info.ContainersRunning, Paused: info.Info.ContainersPaused, Stopped: info.Info.ContainersStopped, Images: info.Info.Images, Version: info.Info.ServerVersion, } if ver, verr := c.api.ServerVersion(ctx, client.ServerVersionOptions{}); verr == nil { if ver.Version != "" { out.Version = ver.Version } out.APIVersion = ver.APIVersion if out.Arch == "" { out.Arch = ver.Arch } if out.OS == "" { out.OS = ver.Os } } return out, nil }