mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
200 lines
5.2 KiB
Go
200 lines
5.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/go-units"
|
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
|
)
|
|
|
|
const (
|
|
LabelManaged = "uncloud.managed"
|
|
LabelServiceID = "uncloud.service.id"
|
|
LabelServiceName = "uncloud.service.name"
|
|
LabelServiceMode = "uncloud.service.mode"
|
|
LabelServicePorts = "uncloud.service.ports"
|
|
)
|
|
|
|
type Container struct {
|
|
types.ContainerJSON
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// UncloudNetworkIP returns the IP address of the container in the uncloud Docker network.
|
|
func (c *Container) UncloudNetworkIP() netip.Addr {
|
|
network, ok := c.NetworkSettings.Networks[docker.NetworkName]
|
|
if !ok {
|
|
// Container is not connected to the uncloud Docker network (could be host network).
|
|
return netip.Addr{}
|
|
}
|
|
|
|
ip, err := netip.ParseAddr(network.IPAddress)
|
|
if err != nil {
|
|
return netip.Addr{}
|
|
}
|
|
|
|
return ip
|
|
}
|
|
|
|
func (c *Container) UnmarshalJSON(data []byte) error {
|
|
// A temporary type that's identical to Container but doesn't have the UnmarshalJSON method.
|
|
type ContainerAlias Container
|
|
|
|
var temp ContainerAlias
|
|
if err := json.Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
|
|
*c = Container(temp)
|
|
c.Name = strings.TrimPrefix(c.Name, "/")
|
|
|
|
return nil
|
|
}
|
|
|
|
type ServiceContainer struct {
|
|
Container
|
|
ServiceSpec ServiceSpec
|
|
}
|
|
|
|
// ServiceID returns the ID of the service this container belongs to.
|
|
func (c *ServiceContainer) ServiceID() string {
|
|
return c.Config.Labels[LabelServiceID]
|
|
}
|
|
|
|
// ServiceName returns the name of the service this container belongs to.
|
|
func (c *ServiceContainer) ServiceName() string {
|
|
return c.Config.Labels[LabelServiceName]
|
|
}
|
|
|
|
// ServiceMode returns the replication mode of the service this container belongs to.
|
|
func (c *ServiceContainer) ServiceMode() string {
|
|
return c.Config.Labels[LabelServiceMode]
|
|
}
|
|
|
|
// ServicePorts returns the ports this container publishes as part of its service.
|
|
func (c *ServiceContainer) 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
|
|
}
|
|
|
|
// ConflictingServicePorts returns a list of service ports that conflict with the given ports.
|
|
func (c *ServiceContainer) 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
|
|
}
|