refactor: move api, client, compose packages to pkg

This commit is contained in:
Pavel Sviderski
2025-03-22 18:43:00 +10:00
parent 3757813b20
commit b1abd07dde
42 changed files with 82 additions and 82 deletions
+189
View File
@@ -0,0 +1,189 @@
package api
import (
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
"strings"
"time"
)
const (
LabelManaged = "uncloud.managed"
LabelServiceID = "uncloud.service.id"
LabelServiceName = "uncloud.service.name"
LabelServiceMode = "uncloud.service.mode"
LabelServicePorts = "uncloud.service.ports"
LabelServiceSpecHash = "uncloud.service.spec-hash"
)
type Container struct {
types.ContainerJSON
}
// NameWithoutSlash returns the container name without the leading slash.
// TODO: modify Name in original ContainerJSON structure when inspecting a Docker container and get rid of this method.
func (c *Container) NameWithoutSlash() string {
return c.Name[1:]
}
// ServiceID returns the ID of the service this container belongs to.
func (c *Container) ServiceID() string {
return c.Config.Labels[LabelServiceID]
}
// ServiceName returns the name of the service this container belongs to.
func (c *Container) ServiceName() string {
return c.Config.Labels[LabelServiceName]
}
// ServiceMode returns the replication mode of the service this container belongs to.
func (c *Container) ServiceMode() string {
return c.Config.Labels[LabelServiceMode]
}
// ServicePorts returns the ports this container publishes as part of its service.
func (c *Container) ServicePorts() ([]PortSpec, error) {
encoded, ok := c.Config.Labels[LabelServicePorts]
if !ok {
return nil, nil
}
if strings.TrimSpace(encoded) == "" {
return nil, nil
}
publishPorts := strings.Split(encoded, ",")
ports := make([]PortSpec, len(publishPorts))
for i, p := range publishPorts {
port, err := ParsePortSpec(strings.TrimSpace(p))
if err != nil {
return nil, err
}
ports[i] = port
}
return ports, nil
}
// ServiceSpec constructs a service spec from the container's configuration.
func (c *Container) ServiceSpec() (ServiceSpec, error) {
ports, err := c.ServicePorts()
if err != nil {
return ServiceSpec{}, fmt.Errorf("get service ports: %w", err)
}
// TODO: many properties on the container such as Config.Cmd or Config.Entrypoint are populated when the container
// is created. Figure out how to get a spec that is equal to the initial spec.
return ServiceSpec{
Container: ContainerSpec{
Command: c.Config.Cmd,
Entrypoint: c.Config.Entrypoint,
Image: c.Config.Image,
Init: c.HostConfig.Init,
Volumes: c.HostConfig.Binds,
},
Mode: c.ServiceMode(),
Name: c.ServiceName(),
Ports: ports,
}, nil
}
// Healthy determines if the container is running and healthy.
// A running container with no health check configured is considered healthy.
func (c *Container) Healthy() bool {
if !c.State.Running || c.State.Paused || c.State.Restarting {
return false
}
// If there's no health status (no health check configured), container is considered healthy.
if c.State.Health == nil {
return true
}
return c.State.Health.Status == types.Healthy
}
// HumanState returns a human-readable description of the container's state. Based on the Docker implementation:
// https://github.com/moby/moby/blob/b343d235a0a1f30c8f05b1d651238e72158dc25d/container/state.go#L79-L113
func (c *Container) HumanState() (string, error) {
startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt)
if err != nil {
return "", fmt.Errorf("parse started time: %w", err)
}
finishedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt)
if err != nil {
return "", fmt.Errorf("parse finished time: %w", err)
}
if c.State.Running {
if c.State.Paused {
return fmt.Sprintf("Up %s (Paused)", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
}
if c.State.Restarting {
return fmt.Sprintf("Restarting (%d) %s ago",
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
}
if c.State.Health != nil {
status := c.State.Health.Status
if status == types.Starting {
status = "health: " + status
}
return fmt.Sprintf("Up %s (%s)", units.HumanDuration(time.Now().UTC().Sub(startedAt)), status), nil
}
return fmt.Sprintf("Up %s", units.HumanDuration(time.Now().UTC().Sub(startedAt))), nil
}
if c.State.Status == "removing" {
return "Removal In Progress", nil
}
if c.State.Dead {
return "Dead", nil
}
if startedAt.IsZero() {
return "Created", nil
}
if finishedAt.IsZero() {
return "", nil
}
return fmt.Sprintf("Exited (%d) %s ago",
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
}
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
svcPorts, err := c.ServicePorts()
if err != nil {
return nil, fmt.Errorf("get service ports: %w", err)
}
var conflicting []PortSpec
for _, p := range ports {
if p.Mode != PortModeHost {
continue
}
// Two host ports conflict if they have the same published port number and protocol, and either:
// * At least one host IP is not set (meaning it uses all interfaces)
// * Both host IPs are identical
for _, svcPort := range svcPorts {
if svcPort.Mode != PortModeHost ||
svcPort.PublishedPort != p.PublishedPort ||
svcPort.Protocol != p.Protocol {
continue
}
if !svcPort.HostIP.IsValid() || !p.HostIP.IsValid() || svcPort.HostIP.Compare(p.HostIP) == 0 {
conflicting = append(conflicting, p)
}
}
}
return conflicting, nil
}