mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-27 19:43:34 +00:00
refactor: api.Container to use the detailed ContainerJSON struct instead of Container (Summary)
This commit is contained in:
+63
-43
@@ -3,8 +3,9 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"regexp"
|
||||
"github.com/docker/go-units"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -13,42 +14,30 @@ const (
|
||||
LabelServiceName = "uncloud.service.name"
|
||||
LabelServiceMode = "uncloud.service.mode"
|
||||
LabelServicePorts = "uncloud.service.ports"
|
||||
|
||||
StateCreated = "created"
|
||||
StateDead = "dead"
|
||||
StateExited = "exited"
|
||||
StatePaused = "paused"
|
||||
StateRemoving = "removing"
|
||||
StateRestarting = "restarting"
|
||||
StateRunning = "running"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
types.Container
|
||||
}
|
||||
|
||||
func (c *Container) Name() string {
|
||||
return c.Names[0][1:] // Remove leading slash.
|
||||
types.ContainerJSON
|
||||
}
|
||||
|
||||
// ServiceID returns the ID of the service this container belongs to.
|
||||
func (c *Container) ServiceID() string {
|
||||
return c.Labels[LabelServiceID]
|
||||
return c.Config.Labels[LabelServiceID]
|
||||
}
|
||||
|
||||
// ServiceName returns the name of the service this container belongs to.
|
||||
func (c *Container) ServiceName() string {
|
||||
return c.Labels[LabelServiceName]
|
||||
return c.Config.Labels[LabelServiceName]
|
||||
}
|
||||
|
||||
// ServiceMode returns the replication mode of the service this container belongs to.
|
||||
func (c *Container) ServiceMode() string {
|
||||
return c.Labels[LabelServiceMode]
|
||||
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.Labels[LabelServicePorts]
|
||||
encoded, ok := c.Config.Labels[LabelServicePorts]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -75,41 +64,72 @@ func (c *Container) ServiceSpec() ServiceSpec {
|
||||
return ServiceSpec{}
|
||||
}
|
||||
|
||||
// runningStatusRegex matches the status string of a running container.
|
||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
||||
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
|
||||
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
|
||||
// - "Restarting (0) 5 seconds ago" -> no match
|
||||
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
|
||||
// for more details on how the status string for a running container is formatted.
|
||||
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
|
||||
|
||||
// Healthy determines if the container is running and healthy based on its status string.
|
||||
// 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 != StateRunning {
|
||||
if !c.State.Running || c.State.Paused || c.State.Restarting {
|
||||
return false
|
||||
}
|
||||
|
||||
matches := runningStatusRegex.FindStringSubmatch(c.Status)
|
||||
// Not "Up" or invalid format.
|
||||
if matches == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
|
||||
if matches[1] == "" {
|
||||
// If there's no health status (no health check configured), container is considered healthy.
|
||||
if c.State.Health == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the health status in parentheses is "healthy", the container is considered healthy.
|
||||
return matches[1] == types.Healthy
|
||||
return c.State.Health.Status == types.Healthy
|
||||
}
|
||||
|
||||
// Stopped determines if the container is stopped and doesn't try to restart.
|
||||
func (c *Container) Stopped() bool {
|
||||
return c.State == StateCreated || c.State == StateDead || c.State == StateExited
|
||||
// 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.
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/netip"
|
||||
@@ -13,81 +14,111 @@ func TestContainer_Healthy(t *testing.T) {
|
||||
|
||||
t.Run("exited", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "exited",
|
||||
Status: "Exited (0) 2 minutes ago",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: false,
|
||||
Dead: false,
|
||||
ExitCode: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("running with no health check", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up 5 minutes",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.True(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("running and healthy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up 3 minutes (healthy)",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
Health: &types.Health{
|
||||
Status: types.Healthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.True(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("running but unhealthy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up 2 hours (unhealthy)",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
Health: &types.Health{
|
||||
Status: types.Unhealthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("running with health starting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up 1 minute (health: starting)",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Running: true,
|
||||
Health: &types.Health{
|
||||
Status: "starting",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("invalid up format no time", func(t *testing.T) {
|
||||
t.Run("dead", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up",
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("invalid up format empty parentheses", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Up 5 minutes ()",
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("malformed status", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Invalid status",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Dead: true,
|
||||
Running: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("restarting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{Container: types.Container{
|
||||
State: "running",
|
||||
Status: "Restarting (0) 5 seconds ago",
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Restarting: true,
|
||||
Running: true,
|
||||
ExitCode: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
|
||||
t.Run("paused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &Container{ContainerJSON: types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{
|
||||
Paused: true,
|
||||
Running: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
assert.False(t, c.Healthy())
|
||||
})
|
||||
@@ -248,15 +279,15 @@ func TestContainer_ConflictingServicePorts(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
container := &Container{
|
||||
Container: types.Container{
|
||||
ctr := &Container{ContainerJSON: types.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
LabelServicePorts: tt.containerPorts,
|
||||
},
|
||||
},
|
||||
}
|
||||
}}
|
||||
|
||||
got, err := container.ConflictingServicePorts(tt.checkPorts)
|
||||
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
|
||||
@@ -233,7 +233,7 @@ func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID
|
||||
}
|
||||
|
||||
for _, c := range svc.Containers {
|
||||
if c.Container.ID == containerID || c.Container.Name() == containerID {
|
||||
if c.Container.ID == containerID || c.Container.Name == containerID {
|
||||
ctr = c
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||
|
||||
pw.Event(progress.StartingEvent(eventID))
|
||||
if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil {
|
||||
@@ -285,7 +285,7 @@ func (cli *Client) StopContainer(
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||
|
||||
pw.Event(progress.StoppingEvent(eventID))
|
||||
if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||
@@ -312,7 +312,7 @@ func (cli *Client) RemoveContainer(
|
||||
ctx = proxyToMachine(ctx, machine.Machine)
|
||||
|
||||
pw := progress.ContextWriter(ctx)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name(), machine.Machine.Name)
|
||||
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name)
|
||||
|
||||
pw.Event(progress.RemovingEvent(eventID))
|
||||
if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil {
|
||||
|
||||
@@ -288,7 +288,7 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
|
||||
}
|
||||
|
||||
for _, c := range mc.Containers {
|
||||
ctr := api.Container{Container: c}
|
||||
ctr := api.Container{ContainerJSON: c}
|
||||
if ctr.ServiceID() == id || ctr.ServiceName() == id {
|
||||
containers = append(containers, api.MachineContainer{
|
||||
MachineID: machineID,
|
||||
@@ -455,7 +455,7 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
|
||||
}
|
||||
|
||||
for _, c := range mc.Containers {
|
||||
ctr := api.Container{Container: c}
|
||||
ctr := api.Container{ContainerJSON: c}
|
||||
if _, ok := servicesByID[ctr.ServiceID()]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ func (s *RollingStrategy) planGlobal(
|
||||
}
|
||||
|
||||
plan := &SequenceOperation{}
|
||||
// TODO: figure out how to return a warning if there are machines down.
|
||||
var machinesDown []*pb.MachineInfo
|
||||
for _, m := range machines {
|
||||
// Skip machines that are down but collect them to report a warning later.
|
||||
@@ -93,6 +94,9 @@ func (s *RollingStrategy) planGlobal(
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// reconcileGlobalContainer returns a sequence of operations to reconcile containers on a machine for a global service.
|
||||
// It ensures exactly one container with the desired spec is running on the machine by creating a new container and
|
||||
// removing old ones. If there is a host port conflict, it stops the old container before starting a new one.
|
||||
func reconcileGlobalContainer(
|
||||
containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string,
|
||||
) ([]Operation, error) {
|
||||
@@ -111,7 +115,7 @@ func reconcileGlobalContainer(
|
||||
// Check if there is a container with the same spec already running. If so, remove the rest.
|
||||
upToDate := false
|
||||
for i, c := range containers {
|
||||
if c.Container.State != api.StateRunning && c.Container.State != api.StateRestarting {
|
||||
if !c.Container.State.Running || c.Container.State.Paused {
|
||||
// Skip containers that are not running.
|
||||
continue
|
||||
}
|
||||
@@ -138,9 +142,9 @@ func reconcileGlobalContainer(
|
||||
}
|
||||
|
||||
// The machine has containers but none of them match the new spec.
|
||||
// Stop the old non-stopped containers that have conflicting ports with the new spec before running a new one.
|
||||
// Stop the old running containers that have conflicting ports with the new spec before running a new one.
|
||||
for _, c := range containers {
|
||||
if !c.Container.Stopped() {
|
||||
if c.Container.State.Running {
|
||||
conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check conflicting ports: %w", err)
|
||||
|
||||
@@ -461,7 +461,7 @@ type MachineContainers struct {
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
// JSON serialized []container.Summary.
|
||||
// JSON serialized []container.ContainerJSON.
|
||||
Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ message ListContainersResponse {
|
||||
|
||||
message MachineContainers {
|
||||
Metadata metadata = 1;
|
||||
// JSON serialized []container.Summary.
|
||||
// JSON serialized []container.ContainerJSON.
|
||||
bytes containers = 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,18 +89,18 @@ func (c *Controller) Run(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// filterAvailableContainers filters out containers that are likely unavailable from this machine. The availability
|
||||
// filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability
|
||||
// is determined by the cluster membership state of the machine that the container is running on.
|
||||
// TODO: implement machine membership check using Corrossion Admin client.
|
||||
func (c *Controller) filterAvailableContainers(containerRecords []*store.ContainerRecord) ([]*api.Container, error) {
|
||||
containers := make([]*api.Container, len(containerRecords))
|
||||
func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) {
|
||||
containers := make([]api.Container, len(containerRecords))
|
||||
for i, cr := range containerRecords {
|
||||
containers[i] = cr.Container
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (c *Controller) generateConfig(containers []*api.Container) error {
|
||||
func (c *Controller) generateConfig(containers []api.Container) error {
|
||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||
httpHostUpstreams := make(map[string][]string)
|
||||
httpsHostUpstreams := make(map[string][]string)
|
||||
|
||||
@@ -150,7 +150,7 @@ func (c *Client) StopContainer(ctx context.Context, id string, opts container.St
|
||||
|
||||
type MachineContainers struct {
|
||||
Metadata *pb.Metadata
|
||||
Containers []types.Container
|
||||
Containers []types.ContainerJSON
|
||||
}
|
||||
|
||||
func (c *Client) ListContainers(ctx context.Context, opts container.ListOptions) ([]MachineContainers, error) {
|
||||
|
||||
@@ -149,8 +149,9 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("list containers from store: %w", err)
|
||||
}
|
||||
|
||||
// List only Uncloud service containers identified by their labels.
|
||||
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||
containerSummaries, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", api.LabelServiceID),
|
||||
filters.Arg("label", api.LabelServiceName),
|
||||
@@ -161,11 +162,21 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||
return fmt.Errorf("list Docker containers: %w", err)
|
||||
}
|
||||
|
||||
// Delete containers that are not present in the Docker daemon from the store.
|
||||
// Inspect each container to get the full container details.
|
||||
containers := make([]api.Container, len(containerSummaries))
|
||||
for i, cs := range containerSummaries {
|
||||
ctr, err := m.client.ContainerInspect(ctx, cs.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect container '%s': %w", cs.ID, err)
|
||||
}
|
||||
containers[i] = api.Container{ContainerJSON: ctr}
|
||||
}
|
||||
|
||||
// Delete containers from the store that are no longer present in the Docker daemon.
|
||||
var deleteIDs []string
|
||||
for _, sc := range storeContainers {
|
||||
found := false
|
||||
for i, _ := range containers {
|
||||
for i := range containers {
|
||||
if containers[i].ID == sc.Container.ID {
|
||||
found = true
|
||||
break
|
||||
@@ -184,8 +195,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Create or update the current Docker containers in the store.
|
||||
for _, dc := range containers {
|
||||
c := &api.Container{Container: dc}
|
||||
for _, c := range containers {
|
||||
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
||||
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ import (
|
||||
)
|
||||
|
||||
// EnsureUncloudNetwork is a stub for darwin.
|
||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
return fmt.Errorf("not supported on darwin")
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
// EnsureUncloudNetwork creates the Docker bridge network NetworkName with the provided machine subnet
|
||||
// if it doesn't exist. If the network exists but has a different subnet, it removes and recreates the network.
|
||||
// It also configures iptables to allow container access from the WireGuard network.
|
||||
func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
func (m *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix) error {
|
||||
// Ensure the Docker network 'uncloud' is created with the correct subnet.
|
||||
needsCreation := false
|
||||
nw, err := d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||
nw, err := m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{})
|
||||
if err != nil {
|
||||
if !client.IsErrNotFound(err) {
|
||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||
@@ -29,7 +29,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
slog.Info(
|
||||
"Removing Docker network with old subnet.", "name", NetworkName, "subnet", nw.IPAM.Config[0].Subnet,
|
||||
)
|
||||
if err = d.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||
if err = m.client.NetworkRemove(ctx, NetworkName); err != nil {
|
||||
// It can still fail if the network is in use by a container. Leave it to the user to resolve the issue.
|
||||
return fmt.Errorf("remove Docker network %q: %w", NetworkName, err)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
}
|
||||
|
||||
if needsCreation {
|
||||
if _, err = d.client.NetworkCreate(
|
||||
if _, err = m.client.NetworkCreate(
|
||||
ctx, NetworkName, dnetwork.CreateOptions{
|
||||
Driver: "bridge",
|
||||
Scope: "local",
|
||||
@@ -54,7 +54,7 @@ func (d *Manager) EnsureUncloudNetwork(ctx context.Context, subnet netip.Prefix)
|
||||
}
|
||||
slog.Info("Docker network created.", "name", NetworkName, "subnet", subnet.String())
|
||||
|
||||
if nw, err = d.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||
if nw, err = m.client.NetworkInspect(ctx, NetworkName, dnetwork.InspectOptions{}); err != nil {
|
||||
return fmt.Errorf("inspect Docker network %q: %w", NetworkName, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
@@ -144,10 +145,18 @@ func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersReque
|
||||
}
|
||||
}
|
||||
|
||||
containers, err := s.client.ContainerList(ctx, opts)
|
||||
containerSummaries, err := s.client.ContainerList(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
containers := make([]types.ContainerJSON, len(containerSummaries))
|
||||
for i, cs := range containerSummaries {
|
||||
c, err := s.client.ContainerInspect(ctx, cs.ID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
|
||||
}
|
||||
containers[i] = c
|
||||
}
|
||||
|
||||
containersBytes, err := json.Marshal(containers)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
type ContainerRecord struct {
|
||||
Container *api.Container
|
||||
Container api.Container
|
||||
MachineID string
|
||||
SyncStatus string
|
||||
UpdatedAt time.Time
|
||||
@@ -47,8 +47,11 @@ type DeleteOptions struct {
|
||||
|
||||
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
||||
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, machineID string) error {
|
||||
cJSON, err := json.Marshal(c)
|
||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, ctr api.Container, machineID string) error {
|
||||
// Remove the environment variables from the container record before storing it in the database
|
||||
// to avoid leaking secrets.
|
||||
ctr.Config.Env = nil
|
||||
cJSON, err := json.Marshal(ctr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal container: %w", err)
|
||||
}
|
||||
@@ -63,19 +66,19 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *api.Container, m
|
||||
updated_at = excluded.updated_at
|
||||
WHERE containers.container != excluded.container
|
||||
OR containers.machine_id != excluded.machine_id`,
|
||||
c.ID, string(cJSON), machineID, SyncStatusSynced)
|
||||
ctr.ID, string(cJSON), machineID, SyncStatusSynced)
|
||||
if err != nil {
|
||||
return fmt.Errorf("upsert query: %w", err)
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
slog.Debug("Container record updated in store DB.", "id", c.ID, "machine_id", machineID)
|
||||
slog.Debug("Container record updated in store DB.", "id", ctr.ID, "machine_id", machineID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListContainers returns a list of container records from the store database that match the given options.
|
||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*ContainerRecord, error) {
|
||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]ContainerRecord, error) {
|
||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||
|
||||
@@ -105,7 +108,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var containers []*ContainerRecord
|
||||
var containers []ContainerRecord
|
||||
var cJSON, machineID, syncStatus, updatedAtStr string
|
||||
var updatedAt time.Time
|
||||
|
||||
@@ -121,8 +124,8 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
||||
if updatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||
return nil, fmt.Errorf("parse updated_at: %w", err)
|
||||
}
|
||||
containers = append(containers, &ContainerRecord{
|
||||
Container: &c,
|
||||
containers = append(containers, ContainerRecord{
|
||||
Container: c,
|
||||
MachineID: machineID,
|
||||
SyncStatus: syncStatus,
|
||||
UpdatedAt: updatedAt,
|
||||
@@ -158,7 +161,7 @@ func (s *Store) DeleteContainers(ctx context.Context, opts DeleteOptions) error
|
||||
|
||||
// SubscribeContainers returns a list of containers and a channel that signals changes to the list. The channel doesn't
|
||||
// receive any values, it just signals when a container(s) has been added, updated, or deleted in the database.
|
||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-chan struct{}, error) {
|
||||
func (s *Store) SubscribeContainers(ctx context.Context) ([]ContainerRecord, <-chan struct{}, error) {
|
||||
// TODO: figure out whether we need sync_status at all.
|
||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers").
|
||||
Where(sq.Eq{"sync_status": SyncStatusSynced})
|
||||
@@ -172,7 +175,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var containers []*ContainerRecord
|
||||
var containers []ContainerRecord
|
||||
var cJSON, updatedAtStr string
|
||||
|
||||
rows := sub.Rows()
|
||||
@@ -188,7 +191,7 @@ func (s *Store) SubscribeContainers(ctx context.Context) ([]*ContainerRecord, <-
|
||||
if cr.UpdatedAt, err = time.Parse(time.DateTime, updatedAtStr); err != nil {
|
||||
return nil, nil, fmt.Errorf("parse updated_at: %w", err)
|
||||
}
|
||||
containers = append(containers, &cr)
|
||||
containers = append(containers, cr)
|
||||
}
|
||||
events, err := sub.Changes()
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,8 @@ CREATE TABLE containers
|
||||
-- container is a JSON-serialized api.Container struct.
|
||||
container TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(container)),
|
||||
machine_id TEXT NOT NULL DEFAULT '',
|
||||
service_id TEXT AS (json_extract(container, '$.Labels."uncloud.service.id"')),
|
||||
service_name TEXT AS (json_extract(container, '$.Labels."uncloud.service.name"')),
|
||||
service_id TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.id"')),
|
||||
service_name TEXT AS (json_extract(container, '$.Config.Labels."uncloud.service.name"')),
|
||||
-- sync_status indicates if the record reflects the actual Docker state of the container.
|
||||
sync_status TEXT NOT NULL DEFAULT '',
|
||||
-- updated_at is the last time the record was updated.
|
||||
|
||||
Reference in New Issue
Block a user