feat(pre-deploy): add support for pre-deploy hook containers in service management, sync *all* containers to store

This commit is contained in:
Pasha Sviderski
2026-04-08 19:16:28 +10:00
parent cef047221c
commit 1c8b77054a
11 changed files with 508 additions and 298 deletions
+46 -4
View File
@@ -7,14 +7,17 @@ import (
"strings"
"time"
"github.com/containerd/errdefs"
"encoding/json"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/psviderski/uncloud/internal/docker"
"github.com/psviderski/uncloud/internal/machine/api/pb"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -24,6 +27,27 @@ import (
// CreateContainer creates a new container for the given service on the specified machine.
func (cli *Client) CreateContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, machineID string,
) (container.CreateResponse, error) {
return cli.createServiceContainerWithPull(ctx, serviceID, spec, machineID, pb.CreateServiceContainerRequest_SERVICE)
}
// CreatePreDeployHookContainer creates a one-shot container for a pre-deploy hook for the given service
// on the specified machine.
func (cli *Client) CreatePreDeployHookContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, machineID string,
) (container.CreateResponse, error) {
return cli.createServiceContainerWithPull(
ctx, serviceID, spec, machineID, pb.CreateServiceContainerRequest_PRE_DEPLOY)
}
// createServiceContainerWithPull creates a regular or deployment hook container for the service
// on the specified machine, pulling the image if needed.
func (cli *Client) createServiceContainerWithPull(
ctx context.Context,
serviceID string,
spec api.ServiceSpec,
machineID string,
containerType pb.CreateServiceContainerRequest_ContainerType,
) (container.CreateResponse, error) {
var resp container.CreateResponse
@@ -43,6 +67,9 @@ func (cli *Client) CreateContainer(
return resp, fmt.Errorf("generate random suffix: %w", err)
}
containerName := fmt.Sprintf("%s-%s", spec.Name, suffix)
if containerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
containerName = fmt.Sprintf("%s-%s-%s", spec.Name, api.LabelHookPreDeploy, suffix)
}
// Proxy Docker gRPC requests to the selected machine.
ctx = proxyToMachine(ctx, machine.Machine)
@@ -57,7 +84,18 @@ func (cli *Client) CreateContainer(
}
}
resp, err = cli.Docker.CreateServiceContainer(ctx, serviceID, spec, containerName)
specBytes, err := json.Marshal(spec)
if err != nil {
return resp, fmt.Errorf("marshal service spec: %w", err)
}
req := &pb.CreateServiceContainerRequest{
ServiceId: serviceID,
ServiceSpec: specBytes,
ContainerName: containerName,
ContainerType: containerType,
}
grpcResp, err := cli.Docker.GRPCClient.CreateServiceContainer(ctx, req)
if err != nil {
switch spec.Container.PullPolicy {
case api.PullPolicyAlways, api.PullPolicyNever:
@@ -68,7 +106,7 @@ func (cli *Client) CreateContainer(
}
// NotFound (No such image) error is expected if the image is missing.
if !errdefs.IsNotFound(err) || !strings.Contains(err.Error(), "No such image") {
if status.Code(err) != codes.NotFound || !strings.Contains(err.Error(), "No such image") {
return resp, err
}
@@ -76,10 +114,14 @@ func (cli *Client) CreateContainer(
if err = cli.pullImageWithProgress(ctx, spec.Container.Image, machine.Machine.Name, eventID); err != nil {
return resp, err
}
if resp, err = cli.Docker.CreateServiceContainer(ctx, serviceID, spec, containerName); err != nil {
if grpcResp, err = cli.Docker.GRPCClient.CreateServiceContainer(ctx, req); err != nil {
return resp, err
}
}
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
}
pw.Event(progress.CreatedEvent(eventID))
return resp, nil
+24 -17
View File
@@ -109,7 +109,7 @@ func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Ser
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List all service containers including stopped ones.
// List all service containers including stopped ones and deployment hooks.
opts := container.ListOptions{All: true}
machineContainers, err := cli.Docker.ListServiceContainers(listCtx, nameOrID, opts)
if err != nil {
@@ -146,16 +146,15 @@ func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Ser
}
}
for _, ctr := range mc.Containers {
if ctr.ServiceID() == nameOrID || ctr.ServiceName() == nameOrID {
containers = append(containers, api.MachineServiceContainer{
MachineID: machineID,
Container: ctr,
})
// Collect both regular and hook containers for the service.
for _, ctr := range append(mc.Containers, mc.HookContainers...) {
containers = append(containers, api.MachineServiceContainer{
MachineID: machineID,
Container: ctr,
})
if ctr.ServiceID() == nameOrID {
foundByID = true
}
if ctr.ServiceID() == nameOrID {
foundByID = true
}
}
}
@@ -181,14 +180,22 @@ func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Ser
}
}
svc = api.Service{
ID: containers[0].Container.ServiceID(),
Name: containers[0].Container.ServiceName(),
Mode: containers[0].Container.ServiceMode(),
Containers: containers,
// Partition containers into regular service containers and hook containers.
var serviceContainers, hookContainers []api.MachineServiceContainer
for _, mc := range containers {
if mc.Container.IsHook() {
hookContainers = append(hookContainers, mc)
} else {
serviceContainers = append(serviceContainers, mc)
}
}
if svc.Mode == "" {
svc.Mode = api.ServiceModeReplicated
svc = api.Service{
ID: containers[0].Container.ServiceID(),
Name: containers[0].Container.ServiceName(),
Mode: containers[0].Container.ServiceMode(),
Containers: serviceContainers,
HookContainers: hookContainers,
}
return svc, nil