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
+2 -1
View File
@@ -4,13 +4,14 @@ import (
"context"
"errors"
"fmt"
"os"
"github.com/docker/cli/cli/streams"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"os"
)
// Client is a client for the machine API.
+17 -20
View File
@@ -190,29 +190,26 @@ func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event {
}
// InspectContainer returns the information about the specified container within the service.
func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID string) (api.MachineContainer, error) {
var ctr api.MachineContainer
svc, err := cli.InspectService(ctx, serviceID)
func (cli *Client) InspectContainer(
ctx context.Context, serviceNameOrID, containerNameOrID string,
) (api.MachineServiceContainer, error) {
svc, err := cli.InspectService(ctx, serviceNameOrID)
if err != nil {
return ctr, fmt.Errorf("inspect service: %w", err)
return api.MachineServiceContainer{}, fmt.Errorf("inspect service: %w", err)
}
for _, c := range svc.Containers {
if c.Container.ID == containerID || c.Container.NameWithoutSlash() == containerID {
ctr = c
if c.Container.ID == containerNameOrID || c.Container.Name == containerNameOrID {
return c, nil
}
}
if ctr.MachineID == "" {
return ctr, api.ErrNotFound
}
return ctr, nil
return api.MachineServiceContainer{}, api.ErrNotFound
}
// StartContainer starts the specified container within the service.
func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID string) error {
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
func (cli *Client) StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error {
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
if err != nil {
return err
}
@@ -224,7 +221,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.NameWithoutSlash(), 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 {
@@ -237,9 +234,9 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st
// StopContainer stops the specified container within the service.
func (cli *Client) StopContainer(
ctx context.Context, serviceID, containerID string, opts container.StopOptions,
ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions,
) error {
ctr, err := cli.InspectContainer(ctx, serviceID, containerID)
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
if err != nil {
return err
}
@@ -251,7 +248,7 @@ func (cli *Client) StopContainer(
ctx = proxyToMachine(ctx, machine.Machine)
pw := progress.ContextWriter(ctx)
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), 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 {
@@ -264,9 +261,9 @@ func (cli *Client) StopContainer(
// RemoveContainer removes the specified container within the service.
func (cli *Client) RemoveContainer(
ctx context.Context, serviceID, containerNameOrID string, opts container.RemoveOptions,
ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions,
) error {
ctr, err := cli.InspectContainer(ctx, serviceID, containerNameOrID)
ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID)
if err != nil {
return err
}
@@ -278,7 +275,7 @@ func (cli *Client) RemoveContainer(
ctx = proxyToMachine(ctx, machine.Machine)
pw := progress.ContextWriter(ctx)
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), 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.RemoveServiceContainer(ctx, ctr.Container.ID, opts); err != nil {
+3 -1
View File
@@ -2,6 +2,7 @@ package deploy
import (
"fmt"
"github.com/psviderski/uncloud/pkg/api"
)
@@ -11,7 +12,8 @@ const ContainerUpToDate ContainerSpecStatus = "up-to-date"
const ContainerNeedsUpdate ContainerSpecStatus = "needs-update"
const ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate"
func CompareContainerToSpec(ctr api.Container, spec api.ServiceSpec) (ContainerSpecStatus, error) {
func CompareContainerToSpec(ctr api.ServiceContainer, spec api.ServiceSpec) (ContainerSpecStatus, error) {
// TODO: replace the hash comparison with a more detailed comparison of ctr.ServiceSpec and spec.
specHash, err := spec.ImmutableHash()
if err != nil {
return "", fmt.Errorf("calculate immutable hash for service spec: %w", err)
+7 -6
View File
@@ -3,11 +3,12 @@ package deploy
import (
"context"
"fmt"
"math/rand/v2"
"slices"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"math/rand/v2"
"slices"
)
// Strategy defines how a service should be deployed or updated. Different implementations can provide various
@@ -89,7 +90,7 @@ func (s *RollingStrategy) planReplicated(
})
// Organise existing containers by machine.
containersOnMachine := make(map[string][]api.Container)
containersOnMachine := make(map[string][]api.ServiceContainer)
upToDateContainersOnMachine := make(map[string]int)
containerSpecStatuses := make(map[string]ContainerSpecStatus)
if svc != nil {
@@ -111,7 +112,7 @@ func (s *RollingStrategy) planReplicated(
}
// Sort containers such that running containers with the desired spec are first.
slices.SortFunc(svc.Containers, func(c1, c2 api.MachineContainer) int {
slices.SortFunc(svc.Containers, func(c1, c2 api.MachineServiceContainer) int {
if status, ok := containerSpecStatuses[c1.Container.ID]; ok && status == ContainerUpToDate {
return -1
}
@@ -222,7 +223,7 @@ func (s *RollingStrategy) planGlobal(
// Map machineID to service containers on that machine. For the global mode, there should be at most one
// container per machine but we use a slice to handle multiple containers that may exist due to a bug
// or interruption in the previous deployment.
containersOnMachine := make(map[string][]api.MachineContainer)
containersOnMachine := make(map[string][]api.MachineServiceContainer)
if svc != nil {
for _, c := range svc.Containers {
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c)
@@ -271,7 +272,7 @@ func (s *RollingStrategy) planGlobal(
// 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,
containers []api.MachineServiceContainer, spec api.ServiceSpec, serviceID, machineID string,
) ([]Operation, error) {
var ops []Operation
+2 -1
View File
@@ -3,6 +3,7 @@ package client
import (
"context"
"fmt"
"github.com/psviderski/uncloud/pkg/api"
)
@@ -46,7 +47,7 @@ func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Ser
}
containerNames := make(map[string]string, len(svc.Containers))
for _, c := range svc.Containers {
containerNames[c.Container.ID] = c.Container.NameWithoutSlash()
containerNames[c.Container.ID] = c.Container.Name
}
return NewNameResolver(machineNames, containerNames), nil
+22 -35
View File
@@ -4,17 +4,18 @@ import (
"context"
"errors"
"fmt"
"os"
"slices"
"sync"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"slices"
"sync"
)
type RunServiceResponse struct {
@@ -64,8 +65,8 @@ func (cli *Client) RunService(
}
// InspectService returns detailed information about a service and its containers.
// The id parameter can be either a service ID or name.
func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) {
// The nameOrID parameter can be either a service name or ID.
func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Service, error) {
var svc api.Service
machines, err := cli.ListMachines(ctx)
@@ -87,22 +88,16 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List only uncloud-managed containers that belong to some service.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelManaged),
),
}
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
// List all service containers including stopped ones.
opts := container.ListOptions{All: true}
machineContainers, err := cli.Docker.ListServiceContainers(listCtx, nameOrID, opts)
if err != nil {
return svc, fmt.Errorf("list containers: %w", err)
}
// Collect all containers on all machines that belong to the specified service.
foundByID := false
var containers []api.MachineContainer
var containers []api.MachineServiceContainer
for _, mc := range machineContainers {
// Metadata can be nil if the request was broadcasted to only one machine.
if mc.Metadata == nil && len(machineContainers) > 1 {
@@ -130,15 +125,14 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
}
}
for _, c := range mc.Containers {
ctr := api.Container{ContainerJSON: c}
if ctr.ServiceID() == id || ctr.ServiceName() == id {
containers = append(containers, api.MachineContainer{
for _, ctr := range mc.Containers {
if ctr.ServiceID() == nameOrID || ctr.ServiceName() == nameOrID {
containers = append(containers, api.MachineServiceContainer{
MachineID: machineID,
Container: ctr,
})
if ctr.ServiceID() == id {
if ctr.ServiceID() == nameOrID {
foundByID = true
}
}
@@ -153,15 +147,15 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service,
// may not prevent this), or a service name might match another service's ID. In these cases, matching by ID takes
// priority over matching by name.
if foundByID {
containers = slices.DeleteFunc(containers, func(mc api.MachineContainer) bool {
return mc.Container.ServiceID() != id
containers = slices.DeleteFunc(containers, func(mc api.MachineServiceContainer) bool {
return mc.Container.ServiceID() != nameOrID
})
} else {
// Matched only by name but there could be multiple services with the same name.
serviceID := containers[0].Container.ServiceID()
for _, mc := range containers[1:] {
if mc.Container.ServiceID() != serviceID {
return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", id)
return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", nameOrID)
}
}
}
@@ -273,15 +267,9 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List only uncloud-managed containers that belong to some service.
opts := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", api.LabelServiceID),
filters.Arg("label", api.LabelManaged),
),
}
machineContainers, err := cli.Docker.ListContainers(listCtx, opts)
// List all containers including stopped ones.
opts := container.ListOptions{All: true}
machineContainers, err := cli.Docker.ListServiceContainers(listCtx, "", opts)
if err != nil {
return nil, fmt.Errorf("list containers: %w", err)
}
@@ -292,13 +280,12 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
for _, mc := range machineContainers {
if mc.Metadata != nil && mc.Metadata.Error != "" {
// TODO: return failed machines in the response.
fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n",
fmt.Fprintf(os.Stderr, "WARNING: failed to list containers on machine '%s': %s\n",
mc.Metadata.Machine, mc.Metadata.Error)
continue
}
for _, c := range mc.Containers {
ctr := api.Container{ContainerJSON: c}
for _, ctr := range mc.Containers {
if _, ok := servicesByID[ctr.ServiceID()]; ok {
continue
}