chore: use original service spec instead of deriving from container, fixes diff for scale

This commit is contained in:
Pavel Sviderski
2025-03-30 12:54:36 +10:00
parent 0975cbab66
commit 64f6a4f3d3
22 changed files with 956 additions and 405 deletions
+6 -5
View File
@@ -3,10 +3,11 @@ package api
import (
"context"
"fmt"
"slices"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"google.golang.org/grpc/metadata"
"slices"
)
type Client interface {
@@ -21,10 +22,10 @@ type ContainerClient interface {
CreateContainer(
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
) (container.CreateResponse, error)
InspectContainer(ctx context.Context, serviceID, containerID string) (MachineContainer, error)
RemoveContainer(ctx context.Context, serviceID, containerID string, opts container.RemoveOptions) error
StartContainer(ctx context.Context, serviceID, containerID string) error
StopContainer(ctx context.Context, serviceID, containerID string, opts container.StopOptions) error
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
}
type DNSClient interface {
+68 -70
View File
@@ -1,11 +1,13 @@
package api
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
)
const (
@@ -21,73 +23,6 @@ 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 {
@@ -156,8 +91,71 @@ func (c *Container) HumanState() (string, error) {
c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil
}
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
}
type MachineContainer struct {
MachineID string
Container Container
}
// 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 *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
func (c *ServiceContainer) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) {
svcPorts, err := c.ServicePorts()
if err != nil {
return nil, fmt.Errorf("get service ports: %w", err)
+5 -50
View File
@@ -1,60 +1,15 @@
package api
import (
"net/netip"
"testing"
"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"
"reflect"
"testing"
)
func TestContainer_ServiceSpec(t *testing.T) {
t.Parallel()
init := true
ctr := &Container{ContainerJSON: types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
HostConfig: &container.HostConfig{
Binds: []string{"/host/path:/container/path"},
Init: &init,
},
},
Config: &container.Config{
Cmd: []string{"/app/server"},
Image: "app:latest",
Labels: map[string]string{
LabelServiceID: "test-service-id",
LabelServiceName: "test-service-name",
LabelServicePorts: "app.example.com:8000/https",
},
},
}}
expectedSpec := ServiceSpec{
Container: ContainerSpec{
Command: []string{"/app/server"},
Image: "app:latest",
Init: &init,
Volumes: []string{"/host/path:/container/path"},
},
Name: "test-service-name",
Ports: []PortSpec{
{
Hostname: "app.example.com",
ContainerPort: 8000,
Protocol: ProtocolHTTPS,
Mode: PortModeIngress,
},
},
}
spec, err := ctr.ServiceSpec()
require.NoError(t, err)
assert.True(t, reflect.DeepEqual(spec, expectedSpec))
}
func TestContainer_Healthy(t *testing.T) {
t.Parallel()
@@ -325,13 +280,13 @@ func TestContainer_ConflictingServicePorts(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctr := &Container{ContainerJSON: types.ContainerJSON{
ctr := &ServiceContainer{Container: Container{ContainerJSON: types.ContainerJSON{
Config: &container.Config{
Labels: map[string]string{
LabelServicePorts: tt.containerPorts,
},
},
}}
}}}
got, err := ctr.ConflictingServicePorts(tt.checkPorts)
if tt.wantErr {
+11 -10
View File
@@ -5,12 +5,13 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"github.com/distribution/reference"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"maps"
"reflect"
"regexp"
"slices"
"github.com/distribution/reference"
"github.com/psviderski/uncloud/internal/machine/api/pb"
)
const (
@@ -206,12 +207,12 @@ type Service struct {
ID string
Name string
Mode string
Containers []MachineContainer
Containers []MachineServiceContainer
}
type MachineContainer struct {
type MachineServiceContainer struct {
MachineID string
Container Container
Container ServiceContainer
}
// Endpoints returns the exposed HTTP and HTTPS endpoints of the service.
@@ -261,7 +262,7 @@ func (s *Service) Endpoints() []string {
func ServiceFromProto(s *pb.Service) (Service, error) {
var err error
containers := make([]MachineContainer, len(s.Containers))
containers := make([]MachineServiceContainer, len(s.Containers))
for i, sc := range s.Containers {
containers[i], err = machineContainerFromProto(sc)
if err != nil {
@@ -277,14 +278,14 @@ func ServiceFromProto(s *pb.Service) (Service, error) {
}, nil
}
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
func machineContainerFromProto(sc *pb.Service_Container) (MachineServiceContainer, error) {
var c Container
if err := json.Unmarshal(sc.Container, &c); err != nil {
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
return MachineServiceContainer{}, fmt.Errorf("unmarshal container: %w", err)
}
return MachineContainer{
return MachineServiceContainer{
MachineID: sc.MachineId,
Container: c,
Container: ServiceContainer{Container: c},
}, nil
}