mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
320 lines
9.2 KiB
Go
320 lines
9.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
"github.com/docker/go-units"
|
|
)
|
|
|
|
const (
|
|
// DockerNetworkName is the name of the Docker network used by uncloud. Keep the value in sync with NetworkName
|
|
// in internal/machine/docker/manager.go.
|
|
DockerNetworkName = "uncloud"
|
|
LabelManaged = "uncloud.managed"
|
|
LabelServiceID = "uncloud.service.id"
|
|
LabelServiceName = "uncloud.service.name"
|
|
LabelServiceMode = "uncloud.service.mode"
|
|
LabelServicePorts = "uncloud.service.ports"
|
|
)
|
|
|
|
type Container struct {
|
|
container.InspectResponse
|
|
// created caches the parsed creation time by CreatedTime.
|
|
created time.Time
|
|
}
|
|
|
|
// CreatedTime returns the time when the container was created parsed from the Created field.
|
|
func (c *Container) CreatedTime() time.Time {
|
|
if c.created.IsZero() && c.Created != "" {
|
|
created, err := time.Parse(time.RFC3339Nano, c.Created)
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
c.created = created
|
|
}
|
|
return c.created
|
|
}
|
|
|
|
// HasHealthcheck returns true if the container has a health check configured.
|
|
func (c *Container) HasHealthcheck() bool {
|
|
hc := c.Config.Healthcheck
|
|
if hc == nil {
|
|
return false
|
|
}
|
|
|
|
if len(hc.Test) > 0 && hc.Test[0] == "NONE" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 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 == container.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 == container.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[DockerNetworkName]
|
|
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)
|
|
if c.ContainerJSONBase == nil {
|
|
return fmt.Errorf("container data is missing mandatory base fields: %s", data)
|
|
}
|
|
|
|
c.Name = strings.TrimPrefix(c.Name, "/")
|
|
|
|
return nil
|
|
}
|
|
|
|
type ServiceContainer struct {
|
|
Container
|
|
ServiceSpec ServiceSpec
|
|
}
|
|
|
|
// ShortID returns the truncated ID of the container (12 characters).
|
|
func (c *ServiceContainer) ShortID() string {
|
|
return stringid.TruncateID(c.ID)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// UnmarshalJSON implements custom unmarshalling for ServiceContainer to override the custom unmarshaler
|
|
// of the embedded Container field.
|
|
func (c *ServiceContainer) UnmarshalJSON(data []byte) error {
|
|
// Unmarshal everything except Container into a temporary struct. Keep this in sync with ServiceContainer.
|
|
var temp struct {
|
|
ServiceSpec ServiceSpec
|
|
}
|
|
if err := json.Unmarshal(data, &temp); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Let Container's UnmarshalJSON handle its part.
|
|
if err := json.Unmarshal(data, &c.Container); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.ServiceSpec = temp.ServiceSpec
|
|
|
|
return nil
|
|
}
|
|
|
|
// DefaultHealthMonitorPeriod is the default duration (5 seconds) to wait before checking that the container is still
|
|
// running and not restarting. Can be overridden with the UNCLOUD_HEALTH_MONITOR_PERIOD environment variable
|
|
// (e.g. "10s" or "0").
|
|
var DefaultHealthMonitorPeriod = defaultHealthMonitorPeriod()
|
|
|
|
func defaultHealthMonitorPeriod() time.Duration {
|
|
if v, ok := os.LookupEnv("UNCLOUD_HEALTH_MONITOR_PERIOD"); ok {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
return d
|
|
}
|
|
}
|
|
|
|
return 5 * time.Second
|
|
}
|
|
|
|
// WaitContainerHealthyOptions configures the behaviour of WaitContainerHealthy.
|
|
type WaitContainerHealthyOptions struct {
|
|
// MonitorPeriod is how long to wait before checking that the container is still running and not restarting.
|
|
// Containers with a health check that become healthy before the period ends succeed early.
|
|
// nil means use the default DefaultHealthMonitorPeriod.
|
|
// Zero skips the monitoring and checks the container's health immediately after starting.
|
|
MonitorPeriod *time.Duration
|
|
}
|
|
|
|
// ExecOptions contains configuration for executing a command in a container.
|
|
type ExecOptions struct {
|
|
// Command is the command to run in the container.
|
|
Command []string
|
|
// AttachStdin attaches the stdin stream to the exec session.
|
|
AttachStdin bool
|
|
// AttachStdout attaches the stdout stream to the exec session.
|
|
AttachStdout bool
|
|
// AttachStderr attaches the stderr stream to the exec session.
|
|
AttachStderr bool
|
|
// Tty allocates a pseudo-TTY for the exec session.
|
|
Tty bool
|
|
// Detach runs the command in the background without attaching to streams.
|
|
Detach bool
|
|
|
|
//// Not yet implemented fields
|
|
// User specifies the user to run the command as.
|
|
User string
|
|
// Privileged runs the command in privileged mode.
|
|
Privileged bool
|
|
// WorkingDir sets the working directory for the command.
|
|
WorkingDir string
|
|
// Env sets environment variables for the command.
|
|
Env []string
|
|
|
|
// Client-side only fields (not serialized, not sent to server)
|
|
// Stdin is the input stream. Defaults to os.Stdin if nil.
|
|
Stdin io.Reader `json:"-"`
|
|
// Stdout is the output stream. Defaults to os.Stdout if nil.
|
|
Stdout io.Writer `json:"-"`
|
|
// Stderr is the error stream. Defaults to os.Stderr if nil.
|
|
Stderr io.Writer `json:"-"`
|
|
}
|