mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
727b33c8cc
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
624 lines
20 KiB
Go
624 lines
20 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"encoding/json"
|
|
|
|
"github.com/amir20/dozzle/internal/container"
|
|
"github.com/amir20/dozzle/internal/utils"
|
|
docker "github.com/moby/moby/api/types/container"
|
|
"github.com/moby/moby/api/types/events"
|
|
"github.com/moby/moby/api/types/network"
|
|
"github.com/moby/moby/api/types/system"
|
|
"github.com/moby/moby/client"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type DockerCLI interface {
|
|
ContainerList(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
|
ContainerLogs(context.Context, string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
|
|
Events(context.Context, client.EventsListOptions) client.EventsResult
|
|
ContainerInspect(ctx context.Context, containerID string, options client.ContainerInspectOptions) (client.ContainerInspectResult, error)
|
|
ContainerStats(ctx context.Context, containerID string, options client.ContainerStatsOptions) (client.ContainerStatsResult, error)
|
|
Ping(ctx context.Context, options client.PingOptions) (client.PingResult, error)
|
|
ContainerStart(ctx context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
|
|
ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
|
|
ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error)
|
|
ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error)
|
|
ExecCreate(ctx context.Context, containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error)
|
|
ExecAttach(ctx context.Context, execID string, config client.ExecAttachOptions) (client.ExecAttachResult, error)
|
|
ExecResize(ctx context.Context, execID string, options client.ExecResizeOptions) (client.ExecResizeResult, error)
|
|
Info(ctx context.Context, options client.InfoOptions) (client.SystemInfoResult, error)
|
|
ServerVersion(ctx context.Context, options client.ServerVersionOptions) (client.ServerVersionResult, error)
|
|
ImagePull(ctx context.Context, refStr string, options client.ImagePullOptions) (client.ImagePullResponse, error)
|
|
ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
|
|
ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
|
|
ServiceInspect(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error)
|
|
ServiceUpdate(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error)
|
|
}
|
|
|
|
type DockerClient struct {
|
|
cli DockerCLI
|
|
host container.Host
|
|
info system.Info
|
|
}
|
|
|
|
func NewClient(cli DockerCLI, host container.Host) *DockerClient {
|
|
infoResult, err := cli.Info(context.Background(), client.InfoOptions{})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to get docker info")
|
|
}
|
|
info := infoResult.Info
|
|
|
|
id := info.ID
|
|
if info.Swarm.NodeID != "" {
|
|
id = info.Swarm.NodeID
|
|
}
|
|
|
|
host.ID = id
|
|
host.NCPU = info.NCPU
|
|
host.MemTotal = info.MemTotal
|
|
host.DockerVersion = info.ServerVersion
|
|
host.Runtime = detectRuntime(cli, info)
|
|
host.Swarm = info.Swarm.NodeID != ""
|
|
|
|
return &DockerClient{
|
|
cli: cli,
|
|
host: host,
|
|
info: info,
|
|
}
|
|
}
|
|
|
|
// NewLocalClient creates a new instance of Client with docker filters
|
|
func NewLocalClient(hostname string) (*DockerClient, error) {
|
|
cli, err := client.New(client.FromEnv, client.WithUserAgent("Docker-Client/Dozzle"))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := cli.Ping(context.Background(), client.PingOptions{NegotiateAPIVersion: true}); err != nil {
|
|
return nil, fmt.Errorf("docker daemon unreachable or unsupported (minimum API version %s required): %w", client.MinAPIVersion, err)
|
|
}
|
|
|
|
infoResult, err := cli.Info(context.Background(), client.InfoOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
info := infoResult.Info
|
|
|
|
host := container.Host{
|
|
Name: info.Name,
|
|
Endpoint: "local",
|
|
Type: "local",
|
|
}
|
|
|
|
if hostname != "" {
|
|
host.Name = hostname
|
|
}
|
|
|
|
return NewClient(cli, host), nil
|
|
}
|
|
|
|
func NewRemoteClient(host container.Host) (*DockerClient, error) {
|
|
if host.URL.Scheme != "tcp" {
|
|
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
|
|
}
|
|
|
|
opts := []client.Opt{
|
|
client.WithHost(host.URL.String()),
|
|
}
|
|
|
|
if host.ValidCerts {
|
|
log.Debug().Str("caCertPath", host.CACertPath).Str("certPath", host.CertPath).Str("keyPath", host.KeyPath).Msg("Using TLS for remote client")
|
|
opts = append(opts, client.WithTLSClientConfig(host.CACertPath, host.CertPath, host.KeyPath))
|
|
} else {
|
|
log.Debug().Msg("Not using TLS for remote client")
|
|
}
|
|
|
|
opts = append(opts, client.WithUserAgent("Docker-Client/Dozzle"))
|
|
|
|
cli, err := client.New(opts...)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := cli.Ping(context.Background(), client.PingOptions{NegotiateAPIVersion: true}); err != nil {
|
|
return nil, fmt.Errorf("docker daemon unreachable or unsupported (minimum API version %s required): %w", client.MinAPIVersion, err)
|
|
}
|
|
|
|
host.Type = "remote"
|
|
|
|
return NewClient(cli, host), nil
|
|
}
|
|
|
|
func detectRuntime(cli DockerCLI, info system.Info) string {
|
|
version, err := cli.ServerVersion(context.Background(), client.ServerVersionOptions{})
|
|
if err == nil {
|
|
for _, c := range version.Components {
|
|
if strings.Contains(strings.ToLower(c.Name), "podman") {
|
|
return "podman"
|
|
}
|
|
}
|
|
if strings.Contains(strings.ToLower(version.Platform.Name), "podman") {
|
|
return "podman"
|
|
}
|
|
}
|
|
if strings.Contains(strings.ToLower(info.OperatingSystem), "podman") {
|
|
return "podman"
|
|
}
|
|
return "docker"
|
|
}
|
|
|
|
// Finds a container by id, skipping the filters
|
|
func (d *DockerClient) FindContainer(ctx context.Context, id string) (container.Container, error) {
|
|
log.Debug().Str("id", id).Msg("Finding container")
|
|
if result, err := d.cli.ContainerInspect(ctx, id, client.ContainerInspectOptions{}); err == nil {
|
|
return newContainerFromJSON(result.Container, d.host.ID), nil
|
|
} else {
|
|
return container.Container{}, err
|
|
}
|
|
|
|
}
|
|
|
|
func (d *DockerClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error {
|
|
switch action {
|
|
case container.Start:
|
|
_, err := d.cli.ContainerStart(ctx, containerID, client.ContainerStartOptions{})
|
|
return err
|
|
case container.Stop:
|
|
_, err := d.cli.ContainerStop(ctx, containerID, client.ContainerStopOptions{})
|
|
return err
|
|
case container.Restart:
|
|
_, err := d.cli.ContainerRestart(ctx, containerID, client.ContainerRestartOptions{})
|
|
return err
|
|
case container.Remove:
|
|
_, err := d.cli.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{})
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unknown action: %s", action)
|
|
}
|
|
}
|
|
|
|
func (d *DockerClient) ImagePull(ctx context.Context, imageName string) (io.ReadCloser, error) {
|
|
return d.cli.ImagePull(ctx, imageName, client.ImagePullOptions{})
|
|
}
|
|
|
|
func (d *DockerClient) ContainerInspect(ctx context.Context, containerID string) (docker.InspectResponse, error) {
|
|
result, err := d.cli.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
|
|
return result.Container, err
|
|
}
|
|
|
|
func (d *DockerClient) ContainerRemove(ctx context.Context, containerID string) error {
|
|
_, err := d.cli.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{})
|
|
return err
|
|
}
|
|
|
|
func (d *DockerClient) ContainerCreate(ctx context.Context, inspectResp docker.InspectResponse, name string) (string, error) {
|
|
// Clear hostname when using network modes that don't support it (host, container:*)
|
|
// Docker always populates Hostname in inspect responses, but rejects it on create
|
|
// for these network modes.
|
|
if inspectResp.HostConfig != nil {
|
|
mode := string(inspectResp.HostConfig.NetworkMode)
|
|
if mode == "host" || strings.HasPrefix(mode, "container:") {
|
|
inspectResp.Config.Hostname = ""
|
|
}
|
|
}
|
|
|
|
// Build clean EndpointsConfig with only network names and aliases,
|
|
// stripping runtime state (IPs, gateways, MAC addresses) that can
|
|
// cause conflicts when recreating.
|
|
var networkingConfig *network.NetworkingConfig
|
|
if inspectResp.NetworkSettings != nil && len(inspectResp.NetworkSettings.Networks) > 0 {
|
|
endpointsConfig := make(map[string]*network.EndpointSettings, len(inspectResp.NetworkSettings.Networks))
|
|
for netName, ep := range inspectResp.NetworkSettings.Networks {
|
|
endpointsConfig[netName] = &network.EndpointSettings{
|
|
Aliases: ep.Aliases,
|
|
}
|
|
}
|
|
networkingConfig = &network.NetworkingConfig{EndpointsConfig: endpointsConfig}
|
|
}
|
|
|
|
resp, err := d.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
|
Config: inspectResp.Config,
|
|
HostConfig: inspectResp.HostConfig,
|
|
NetworkingConfig: networkingConfig,
|
|
Platform: nil,
|
|
Name: name,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.ID, nil
|
|
}
|
|
|
|
func (d *DockerClient) ServiceUpdate(ctx context.Context, serviceID string, imageName string) error {
|
|
inspectResult, err := d.cli.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
svc := inspectResult.Service
|
|
svc.Spec.TaskTemplate.ContainerSpec.Image = imageName
|
|
svc.Spec.TaskTemplate.ForceUpdate++
|
|
_, err = d.cli.ServiceUpdate(ctx, serviceID, client.ServiceUpdateOptions{Version: svc.Version, Spec: svc.Spec})
|
|
return err
|
|
}
|
|
|
|
func (d *DockerClient) ListContainers(ctx context.Context, labels container.ContainerLabels) ([]container.Container, error) {
|
|
log.Debug().Interface("labels", labels).Str("host", d.host.Name).Msg("Listing containers")
|
|
filterArgs := make(client.Filters)
|
|
for key, values := range labels {
|
|
filterArgs.Add(key, values...)
|
|
}
|
|
containerListOptions := client.ContainerListOptions{
|
|
Filters: filterArgs,
|
|
All: true,
|
|
}
|
|
list, err := d.cli.ContainerList(ctx, containerListOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var containers = make([]container.Container, 0, len(list.Items))
|
|
for _, c := range list.Items {
|
|
containers = append(containers, newContainer(c, d.host.ID))
|
|
}
|
|
|
|
sort.Slice(containers, func(i, j int) bool {
|
|
return strings.ToLower(containers[i].Name) < strings.ToLower(containers[j].Name)
|
|
})
|
|
|
|
return containers, nil
|
|
}
|
|
|
|
func (d *DockerClient) ContainerStats(ctx context.Context, id string, stats chan<- container.ContainerStat) error {
|
|
response, err := d.cli.ContainerStats(ctx, id, client.ContainerStatsOptions{Stream: true})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
decoder := json.NewDecoder(response.Body)
|
|
var v *docker.StatsResponse
|
|
for {
|
|
if err := decoder.Decode(&v); err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
memPercent, cpuPercent float64
|
|
mem, memLimit float64
|
|
previousCPU uint64
|
|
previousSystem uint64
|
|
networkRx, networkTx uint64
|
|
diskRead, diskWrite uint64
|
|
)
|
|
daemonOSType := v.OSType
|
|
|
|
if daemonOSType != "windows" {
|
|
previousCPU = v.PreCPUStats.CPUUsage.TotalUsage
|
|
previousSystem = v.PreCPUStats.SystemUsage
|
|
cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
|
|
mem = calculateMemUsageUnixNoCache(v.MemoryStats)
|
|
memLimit = float64(v.MemoryStats.Limit)
|
|
memPercent = calculateMemPercentUnixNoCache(memLimit, mem)
|
|
} else {
|
|
cpuPercent = calculateCPUPercentWindows(v)
|
|
mem = float64(v.MemoryStats.PrivateWorkingSet)
|
|
}
|
|
|
|
// Calculate total network bytes across all interfaces
|
|
for _, netStats := range v.Networks {
|
|
networkRx += netStats.RxBytes
|
|
networkTx += netStats.TxBytes
|
|
}
|
|
|
|
// Sum disk read/write bytes across all block devices
|
|
for _, blkio := range v.BlkioStats.IoServiceBytesRecursive {
|
|
switch strings.ToLower(blkio.Op) {
|
|
case "read":
|
|
diskRead += blkio.Value
|
|
case "write":
|
|
diskWrite += blkio.Value
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case stats <- container.ContainerStat{
|
|
ID: id,
|
|
CPUPercent: cpuPercent,
|
|
MemoryPercent: memPercent,
|
|
MemoryUsage: mem,
|
|
NetworkRxTotal: networkRx,
|
|
NetworkTxTotal: networkTx,
|
|
DiskReadTotal: diskRead,
|
|
DiskWriteTotal: diskWrite,
|
|
}:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *DockerClient) ContainerLogs(ctx context.Context, id string, since time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
|
log.Debug().Str("id", id).Time("since", since).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Streaming logs for container")
|
|
|
|
sinceQuery := since.Add(-50 * time.Millisecond).Format(time.RFC3339Nano)
|
|
options := client.ContainerLogsOptions{
|
|
ShowStdout: stdType&container.STDOUT != 0,
|
|
ShowStderr: stdType&container.STDERR != 0,
|
|
Follow: true,
|
|
Tail: strconv.Itoa(100),
|
|
Timestamps: true,
|
|
Since: sinceQuery,
|
|
}
|
|
|
|
reader, err := d.cli.ContainerLogs(ctx, id, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
func (d *DockerClient) ContainerEvents(ctx context.Context, messages chan<- container.ContainerEvent) error {
|
|
eventsResult := d.cli.Events(ctx, client.EventsListOptions{})
|
|
dockerMessages := eventsResult.Messages
|
|
err := eventsResult.Err
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil
|
|
case err := <-err:
|
|
return err
|
|
|
|
case message := <-dockerMessages:
|
|
if message.Type == events.ContainerEventType && len(message.Actor.ID) > 0 {
|
|
messages <- container.ContainerEvent{
|
|
ActorID: message.Actor.ID[:12],
|
|
Name: string(message.Action),
|
|
Host: d.host.ID,
|
|
ActorAttributes: message.Actor.Attributes,
|
|
Time: time.Now(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *DockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType container.StdType) (io.ReadCloser, error) {
|
|
log.Debug().Str("id", id).Time("from", from).Time("to", to).Stringer("stdType", stdType).Str("host", d.host.Name).Msg("Fetching logs between dates for container")
|
|
options := client.ContainerLogsOptions{
|
|
ShowStdout: stdType&container.STDOUT != 0,
|
|
ShowStderr: stdType&container.STDERR != 0,
|
|
Timestamps: true,
|
|
Since: from.Format(time.RFC3339Nano),
|
|
Until: to.Format(time.RFC3339Nano),
|
|
}
|
|
|
|
reader, err := d.cli.ContainerLogs(ctx, id, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reader, nil
|
|
}
|
|
|
|
func (d *DockerClient) Ping(ctx context.Context) error {
|
|
_, err := d.cli.Ping(ctx, client.PingOptions{})
|
|
return err
|
|
}
|
|
|
|
func (d *DockerClient) Host() container.Host {
|
|
return d.host
|
|
}
|
|
|
|
// RawClient returns the underlying *client.Client if the DockerCLI is one.
|
|
// Needed for operations like network/volume management that aren't part of
|
|
// the DockerCLI interface.
|
|
func (d *DockerClient) RawClient() *client.Client {
|
|
if c, ok := d.cli.(*client.Client); ok {
|
|
return c
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DockerClient) ContainerAttach(ctx context.Context, id string) (*container.ExecSession, error) {
|
|
log.Debug().Str("id", id).Str("host", d.host.Name).Msg("Attaching to container")
|
|
options := client.ContainerAttachOptions{
|
|
Stream: true,
|
|
Stdin: true,
|
|
Stdout: true,
|
|
Stderr: true,
|
|
}
|
|
|
|
waiter, err := d.cli.ContainerAttach(ctx, id, options)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Docker attach doesn't support resize - it's not an exec session
|
|
// Return a no-op resize function
|
|
resizeFn := func(width uint, height uint) error {
|
|
log.Debug().Uint("width", width).Uint("height", height).Msg("resize not supported for attach")
|
|
return nil
|
|
}
|
|
|
|
return &container.ExecSession{
|
|
Writer: waiter.Conn,
|
|
Reader: waiter.Reader,
|
|
Resize: resizeFn,
|
|
}, nil
|
|
}
|
|
|
|
func (d *DockerClient) ContainerExec(ctx context.Context, id string, cmd []string) (*container.ExecSession, error) {
|
|
log.Debug().Str("id", id).Str("host", d.host.Name).Msg("Executing command in container")
|
|
options := client.ExecCreateOptions{
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
AttachStdin: true,
|
|
Cmd: cmd,
|
|
TTY: true,
|
|
}
|
|
|
|
execID, err := d.cli.ExecCreate(ctx, id, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
waiter, err := d.cli.ExecAttach(ctx, execID.ID, client.ExecAttachOptions{TTY: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initial resize
|
|
if _, err = d.cli.ExecResize(ctx, execID.ID, client.ExecResizeOptions{
|
|
Width: 100,
|
|
Height: 40,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create resize closure that captures execID and context
|
|
resizeFn := func(width uint, height uint) error {
|
|
_, err := d.cli.ExecResize(ctx, execID.ID, client.ExecResizeOptions{
|
|
Width: width,
|
|
Height: height,
|
|
})
|
|
return err
|
|
}
|
|
|
|
return &container.ExecSession{
|
|
Writer: waiter.Conn,
|
|
Reader: waiter.Reader,
|
|
Resize: resizeFn,
|
|
}, nil
|
|
}
|
|
|
|
func newContainer(c docker.Summary, host string) container.Container {
|
|
name := "no name"
|
|
if c.Labels["dev.dozzle.name"] != "" {
|
|
name = c.Labels["dev.dozzle.name"]
|
|
} else if c.Labels["coolify.serviceName"] != "" {
|
|
name = c.Labels["coolify.serviceName"]
|
|
} else if len(c.Names) > 0 {
|
|
name = strings.TrimPrefix(c.Names[0], "/")
|
|
}
|
|
|
|
group := ""
|
|
if c.Labels["dev.dozzle.group"] != "" {
|
|
group = c.Labels["dev.dozzle.group"]
|
|
} else if c.Labels["coolify.projectName"] != "" {
|
|
group = c.Labels["coolify.projectName"]
|
|
}
|
|
return container.Container{
|
|
ID: c.ID[:12],
|
|
Name: name,
|
|
Image: c.Image,
|
|
Command: c.Command,
|
|
Created: time.Unix(c.Created, 0),
|
|
State: string(c.State),
|
|
Host: host,
|
|
Labels: c.Labels,
|
|
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
|
Group: group,
|
|
}
|
|
}
|
|
|
|
func newContainerFromJSON(c docker.InspectResponse, host string) container.Container {
|
|
name := "no name"
|
|
if c.Config.Labels["dev.dozzle.name"] != "" {
|
|
name = c.Config.Labels["dev.dozzle.name"]
|
|
} else if c.Config.Labels["coolify.serviceName"] != "" {
|
|
name = c.Config.Labels["coolify.serviceName"]
|
|
} else if len(c.Name) > 0 {
|
|
name = strings.TrimPrefix(c.Name, "/")
|
|
}
|
|
|
|
group := ""
|
|
if c.Config.Labels["dev.dozzle.group"] != "" {
|
|
group = c.Config.Labels["dev.dozzle.group"]
|
|
} else if c.Config.Labels["coolify.projectName"] != "" {
|
|
group = c.Config.Labels["coolify.projectName"]
|
|
}
|
|
|
|
// Format port bindings as readable strings
|
|
var ports []string
|
|
for port, bindings := range c.HostConfig.PortBindings {
|
|
for _, b := range bindings {
|
|
if b.HostPort != "" {
|
|
ports = append(ports, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port))
|
|
} else {
|
|
ports = append(ports, port.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Capture mounts in structured form
|
|
mounts := make([]container.Mount, 0, len(c.Mounts))
|
|
for _, m := range c.Mounts {
|
|
mounts = append(mounts, container.Mount{
|
|
Type: string(m.Type),
|
|
Source: m.Source,
|
|
Destination: m.Destination,
|
|
RW: m.RW,
|
|
})
|
|
}
|
|
|
|
restartPolicy := ""
|
|
if c.HostConfig.RestartPolicy.Name != "" {
|
|
restartPolicy = string(c.HostConfig.RestartPolicy.Name)
|
|
}
|
|
|
|
container := container.Container{
|
|
ID: c.ID[:12],
|
|
Name: name,
|
|
Image: c.Config.Image,
|
|
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
|
|
State: string(c.State.Status),
|
|
Host: host,
|
|
Labels: c.Config.Labels,
|
|
Stats: utils.NewRingBuffer[container.ContainerStat](300), // 300 seconds of stats
|
|
Group: group,
|
|
Tty: c.Config.Tty,
|
|
MemoryLimit: uint64(c.HostConfig.Memory),
|
|
CPULimit: float64(c.HostConfig.NanoCPUs) / 1e9,
|
|
Env: c.Config.Env,
|
|
Ports: ports,
|
|
Mounts: mounts,
|
|
RestartPolicy: restartPolicy,
|
|
NetworkMode: string(c.HostConfig.NetworkMode),
|
|
FullyLoaded: true,
|
|
}
|
|
|
|
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
|
container.Created = createdAt.UTC()
|
|
}
|
|
|
|
if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
|
|
container.StartedAt = startedAt.UTC()
|
|
}
|
|
|
|
if stoppedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt); err == nil {
|
|
container.FinishedAt = stoppedAt.UTC()
|
|
}
|
|
|
|
if c.State.Health != nil {
|
|
container.Health = strings.ToLower(string(c.State.Health.Status))
|
|
}
|
|
|
|
return container
|
|
}
|