package docker import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "sort" "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/system" "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" ) // Client wraps the Docker client with additional functionality type Client struct { cli *client.Client probeHost string } // NewClient creates a new Docker client func NewClient() (*Client, error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, fmt.Errorf("failed to create Docker client: %w", err) } // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err = cli.Ping(ctx) if err != nil { return nil, fmt.Errorf("failed to connect to Docker daemon: %w", err) } return &Client{cli: cli}, nil } // HostProbeHost returns an address through which host-published container // ports are reachable. When this process runs inside a container, host // loopback is unreachable; the bridge gateway IP routes to ports published // on 0.0.0.0. Result is cached for the process lifetime. func (c *Client) HostProbeHost(ctx context.Context) string { if c.probeHost != "" { return c.probeHost } host := "127.0.0.1" if self, err := c.self(ctx); err == nil && self.NetworkSettings != nil { for _, n := range self.NetworkSettings.Networks { if n != nil && n.Gateway != "" { host = n.Gateway break } } } c.probeHost = host return host } // OwnNetworkName returns the name of the platform network this process's // container is attached to, or "" when not running containerized. Per-project // and default Docker networks are never selected; among remaining networks // the lexicographically first wins so the choice is deterministic. func (c *Client) OwnNetworkName(ctx context.Context) string { self, err := c.self(ctx) if err != nil || self.NetworkSettings == nil { return "" } names := make([]string, 0, len(self.NetworkSettings.Networks)) for name := range self.NetworkSettings.Networks { switch { case name == "bridge" || name == "host" || name == "none": case strings.HasPrefix(name, "containr-proj-"): default: names = append(names, name) } } sort.Strings(names) if len(names) == 0 { return "" } return names[0] } // self inspects the container this process runs in (hostname == container ID). func (c *Client) self(ctx context.Context) (types.ContainerJSON, error) { hostname, err := os.Hostname() if err != nil || hostname == "" { return types.ContainerJSON{}, fmt.Errorf("hostname unavailable") } return c.cli.ContainerInspect(ctx, hostname) } // ConnectSelfToNetwork attaches this process's container to the given network. // No-op when already attached or when not running containerized. func (c *Client) ConnectSelfToNetwork(ctx context.Context, networkName string) error { hostname, err := os.Hostname() if err != nil || hostname == "" { return fmt.Errorf("hostname unavailable") } err = c.cli.NetworkConnect(ctx, networkName, hostname, &network.EndpointSettings{}) if err != nil && strings.Contains(strings.ToLower(err.Error()), "already exists") { return nil } return err } // ListContainers returns all containers func (c *Client) ListContainers(ctx context.Context, all bool) ([]types.Container, error) { return c.cli.ContainerList(ctx, container.ListOptions{ All: all, }) } // ListContainersFiltered returns containers matching the given filters. func (c *Client) ListContainersFiltered(ctx context.Context, f filters.Args, all bool) ([]types.Container, error) { return c.cli.ContainerList(ctx, container.ListOptions{ All: all, Filters: f, }) } // GetContainer returns detailed information about a specific container func (c *Client) GetContainer(ctx context.Context, containerID string) (types.ContainerJSON, error) { return c.cli.ContainerInspect(ctx, containerID) } // CreateContainer creates a new container func (c *Client) CreateContainer(ctx context.Context, config ContainerConfig) (string, error) { containerConfig := &container.Config{ Image: config.Image, Cmd: config.Cmd, Env: config.Env, Labels: config.Labels, ExposedPorts: config.ExposedPorts, Healthcheck: config.Healthcheck, } hostConfig := &container.HostConfig{ RestartPolicy: container.RestartPolicy{ Name: container.RestartPolicyMode(config.RestartPolicy), }, PortBindings: config.PortBindings, Mounts: config.Mounts, Resources: container.Resources{ Memory: config.Memory, MemoryReservation: config.MemoryReservation, NanoCPUs: config.NanoCPUs, CPUShares: config.CPUShares, }, NetworkMode: container.NetworkMode(config.NetworkMode), } networkingConfig := &network.NetworkingConfig{ EndpointsConfig: config.Networks, } resp, err := c.cli.ContainerCreate( ctx, containerConfig, hostConfig, networkingConfig, nil, config.Name, ) if err != nil { return "", fmt.Errorf("failed to create container: %w", err) } return resp.ID, nil } // StartContainer starts a container func (c *Client) StartContainer(ctx context.Context, containerID string) error { return c.cli.ContainerStart(ctx, containerID, container.StartOptions{}) } // StopContainer stops a container func (c *Client) StopContainer(ctx context.Context, containerID string, timeout *time.Duration) error { var timeoutInt *int if timeout != nil { t := int(timeout.Seconds()) timeoutInt = &t } return c.cli.ContainerStop(ctx, containerID, container.StopOptions{ Timeout: timeoutInt, }) } // RestartContainer restarts a container func (c *Client) RestartContainer(ctx context.Context, containerID string, timeout *time.Duration) error { var timeoutInt *int if timeout != nil { t := int(timeout.Seconds()) timeoutInt = &t } return c.cli.ContainerRestart(ctx, containerID, container.StopOptions{ Timeout: timeoutInt, }) } // RemoveContainer removes a container func (c *Client) RemoveContainer(ctx context.Context, containerID string, force bool) error { return c.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{ Force: force, }) } // GetContainerLogs returns logs for a container func (c *Client) GetContainerLogs(ctx context.Context, containerID string, options LogOptions) (io.ReadCloser, error) { return c.cli.ContainerLogs(ctx, containerID, container.LogsOptions{ ShowStdout: options.Stdout, ShowStderr: options.Stderr, Follow: options.Follow, Tail: options.Tail, Timestamps: options.Timestamps, }) } // CopyFromContainer streams a tar archive of srcPath out of a container. // The container only needs to exist — it does not have to be running. func (c *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, error) { reader, _, err := c.cli.CopyFromContainer(ctx, containerID, srcPath) return reader, err } // CopyToContainer copies a tar archive stream into a container path. func (c *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader) error { return c.cli.CopyToContainer(ctx, containerID, dstPath, content, container.CopyToContainerOptions{}) } // GetContainerStats returns real-time resource usage statistics for a container func (c *Client) GetContainerStats(ctx context.Context, containerID string, stream bool) (*container.StatsResponseReader, error) { resp, err := c.cli.ContainerStats(ctx, containerID, stream) if err != nil { return nil, err } return &resp, nil } // ListImages returns all images func (c *Client) ListImages(ctx context.Context, all bool) ([]image.Summary, error) { return c.cli.ImageList(ctx, image.ListOptions{ All: all, }) } // PullImage pulls an image from a registry. The pull runs as the returned // stream is consumed — callers must drain and close it (or use // PullImageWait) or the pull may be aborted. func (c *Client) PullImage(ctx context.Context, ref string, auth registry.AuthConfig) (io.ReadCloser, error) { authStr, _ := registry.EncodeAuthConfig(auth) return c.cli.ImagePull(ctx, ref, image.PullOptions{ RegistryAuth: authStr, }) } // PullImageWait pulls an image and blocks until the pull stream completes. func (c *Client) PullImageWait(ctx context.Context, ref string, auth registry.AuthConfig) error { reader, err := c.PullImage(ctx, ref, auth) if err != nil { return err } defer reader.Close() // The daemon performs the pull while streaming progress; drain it fully // and surface any errorDetail the daemon reports in the stream. dec := json.NewDecoder(reader) var msg struct { ErrorDetail *struct { Message string `json:"message"` } `json:"errorDetail"` Error string `json:"error"` } for dec.Decode(&msg) == nil { if msg.ErrorDetail != nil { return fmt.Errorf("%s", msg.ErrorDetail.Message) } if msg.Error != "" { return fmt.Errorf("%s", msg.Error) } } return nil } // BuildImage builds an image from a Dockerfile func (c *Client) BuildImage(ctx context.Context, buildContext io.Reader, options BuildOptions) (types.ImageBuildResponse, error) { resp, err := c.cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{ Dockerfile: options.Dockerfile, Tags: options.Tags, BuildArgs: options.BuildArgs, Labels: options.Labels, Remove: options.Remove, NoCache: options.NoCache, }) if err != nil { return resp, err } // The daemon aborts the build if the response stream isn't consumed, // and reports failures only inside that stream. defer resp.Body.Close() var buildErr string dec := json.NewDecoder(resp.Body) for { var line struct { ErrorDetail struct { Message string `json:"message"` } `json:"errorDetail"` Error string `json:"error"` } if err := dec.Decode(&line); err != nil { break } if line.ErrorDetail.Message != "" { buildErr = line.ErrorDetail.Message } else if line.Error != "" { buildErr = line.Error } } if buildErr != "" { return resp, fmt.Errorf("docker build failed: %s", buildErr) } return resp, nil } // RemoveImage removes an image func (c *Client) RemoveImage(ctx context.Context, imageID string, force bool) ([]image.DeleteResponse, error) { return c.cli.ImageRemove(ctx, imageID, image.RemoveOptions{ Force: force, }) } // TagImage tags an image func (c *Client) TagImage(ctx context.Context, imageID, ref string) error { return c.cli.ImageTag(ctx, imageID, ref) } // ListNetworks returns all networks func (c *Client) ListNetworks(ctx context.Context) ([]network.Summary, error) { return c.cli.NetworkList(ctx, network.ListOptions{}) } // CreateNetwork creates a new network func (c *Client) CreateNetwork(ctx context.Context, config NetworkConfig) (string, error) { resp, err := c.cli.NetworkCreate(ctx, config.Name, network.CreateOptions{ Driver: config.Driver, Internal: config.Internal, Labels: config.Labels, }) if err != nil { return "", err } return resp.ID, nil } // RemoveNetwork removes a network func (c *Client) RemoveNetwork(ctx context.Context, networkID string) error { return c.cli.NetworkRemove(ctx, networkID) } // NetworkGateway returns the IPAM gateway of a network — the address // containers on it can use to reach host-published ports. func (c *Client) NetworkGateway(ctx context.Context, networkID string) string { resp, err := c.cli.NetworkInspect(ctx, networkID, network.InspectOptions{}) if err != nil { return "" } for _, cfg := range resp.IPAM.Config { if cfg.Gateway != "" { return cfg.Gateway } } return "" } // ConnectNetwork connects a container to a network func (c *Client) ConnectNetwork(ctx context.Context, networkID, containerID string, config network.EndpointSettings) error { return c.cli.NetworkConnect(ctx, networkID, containerID, &config) } // DisconnectNetwork disconnects a container from a network func (c *Client) DisconnectNetwork(ctx context.Context, networkID, containerID string, force bool) error { return c.cli.NetworkDisconnect(ctx, networkID, containerID, force) } // ListVolumes returns all volumes func (c *Client) ListVolumes(ctx context.Context) (volume.ListResponse, error) { return c.cli.VolumeList(ctx, volume.ListOptions{}) } // CreateVolume creates a new volume func (c *Client) CreateVolume(ctx context.Context, config VolumeConfig) (volume.Volume, error) { return c.cli.VolumeCreate(ctx, volume.CreateOptions{ Name: config.Name, Driver: config.Driver, Labels: config.Labels, DriverOpts: config.DriverOpts, }) } // RemoveVolume removes a volume func (c *Client) RemoveVolume(ctx context.Context, volumeID string, force bool) error { return c.cli.VolumeRemove(ctx, volumeID, force) } // GetSystemInfo returns system-wide information func (c *Client) GetSystemInfo(ctx context.Context) (system.Info, error) { return c.cli.Info(ctx) } // GetDiskUsage returns Docker disk usage information func (c *Client) GetDiskUsage(ctx context.Context) (types.DiskUsage, error) { return c.cli.DiskUsage(ctx, types.DiskUsageOptions{}) } // GetEvents returns Docker events func (c *Client) GetEvents(ctx context.Context, options EventOptions) (io.ReadCloser, error) { resp, errChan := c.cli.Events(ctx, events.ListOptions{ Since: options.Since, Until: options.Until, Filters: options.Filters, }) // Convert the channel to a reader r, w := io.Pipe() go func() { defer w.Close() for { select { case event, ok := <-resp: if !ok { return } // Write event data to pipe w.Write([]byte(fmt.Sprintf("%v\n", event))) case err := <-errChan: if err != nil { w.CloseWithError(err) return } case <-ctx.Done(): return } } }() return r, nil } // ExecCreate creates an exec instance in a container func (c *Client) ExecCreate(ctx context.Context, containerID string, config ExecConfig) (types.IDResponse, error) { return c.cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{ Cmd: config.Cmd, Env: config.Env, WorkingDir: config.WorkingDir, User: config.User, AttachStdin: config.AttachStdin, AttachStdout: config.AttachStdout, AttachStderr: config.AttachStderr, Tty: config.Tty, }) } // ExecStart starts an exec instance func (c *Client) ExecStart(ctx context.Context, execID string, config ExecStartConfig) error { return c.cli.ContainerExecStart(ctx, execID, container.ExecStartOptions{ Detach: config.Detach, Tty: config.Tty, }) } // ExecInspect returns information about an exec instance func (c *Client) ExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) { return c.cli.ContainerExecInspect(ctx, execID) } // ExecRun runs a command in a container and returns its combined // stdout/stderr output and exit code. func (c *Client) ExecRun(ctx context.Context, containerID string, cmd []string) (string, int, error) { exec, err := c.cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{ Cmd: cmd, AttachStdout: true, AttachStderr: true, }) if err != nil { return "", -1, err } resp, err := c.cli.ContainerExecAttach(ctx, exec.ID, container.ExecAttachOptions{}) if err != nil { return "", -1, err } defer resp.Close() var buf bytes.Buffer if _, err := stdcopy.StdCopy(&buf, &buf, resp.Reader); err != nil { return "", -1, err } inspect, err := c.cli.ContainerExecInspect(ctx, exec.ID) if err != nil { return buf.String(), -1, err } return buf.String(), inspect.ExitCode, nil } // ExecAttach attaches to a running exec instance, returning the hijacked // bidirectional stream. With Tty enabled the stream carries raw pty data // (no stdcopy multiplexing). func (c *Client) ExecAttach(ctx context.Context, execID string) (types.HijackedResponse, error) { return c.cli.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{}) } // ExecResize resizes the pty of a running exec instance. func (c *Client) ExecResize(ctx context.Context, execID string, cols, rows uint) error { return c.cli.ContainerExecResize(ctx, execID, container.ResizeOptions{ Height: rows, Width: cols, }) } // GetImageInfo returns information about a Docker image func (c *Client) GetImageInfo(ctx context.Context, imageName string) (*ImageInfo, error) { images, err := c.cli.ImageList(ctx, image.ListOptions{}) if err != nil { return nil, err } for _, img := range images { for _, tag := range img.RepoTags { if tag == imageName || tag == imageName+":latest" { return &ImageInfo{ ID: img.ID, RepoTags: img.RepoTags, Size: img.Size, Created: img.Created, Labels: img.Labels, RepoDigests: img.RepoDigests, Digest: getDigestFromRepoTags(img.RepoDigests), }, nil } } } return nil, fmt.Errorf("image not found: %s", imageName) } // PushImage pushes an image to a registry func (c *Client) PushImage(ctx context.Context, imageName, registryURL string) error { auth := registry.AuthConfig{} authStr, _ := registry.EncodeAuthConfig(auth) _, err := c.cli.ImagePush(ctx, imageName, image.PushOptions{ RegistryAuth: authStr, }) return err } // Close closes the Docker client connection func (c *Client) Close() error { return c.cli.Close() } // Helper function to extract digest from repo digests func getDigestFromRepoTags(digests []string) string { if len(digests) > 0 { return digests[0] } return "" }