refactor: api.Container to use the detailed ContainerJSON struct instead of Container (Summary)

This commit is contained in:
Pavel Sviderski
2025-02-14 16:37:40 +10:00
parent 43f28284f1
commit 5e2b2ac836
18 changed files with 229 additions and 133 deletions
+1 -1
View File
@@ -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"`
}
+1 -1
View File
@@ -67,7 +67,7 @@ message ListContainersResponse {
message MachineContainers {
Metadata metadata = 1;
// JSON serialized []container.Summary.
// JSON serialized []container.ContainerJSON.
bytes containers = 2;
}
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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) {
+15 -5
View File
@@ -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))
}
+1 -1
View File
@@ -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")
}
+5 -5
View File
@@ -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)
}
}
+10 -1
View File
@@ -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 {
+15 -12
View File
@@ -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 {
+2 -2
View File
@@ -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.