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
+24 -38
View File
@@ -420,34 +420,6 @@ func (c *Client) RemoveVolume(ctx context.Context, id string, force bool) error
return err
}
// CreateServiceContainer creates a new container for the service with the given specifications.
func (c *Client) CreateServiceContainer(
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
) (container.CreateResponse, error) {
var resp container.CreateResponse
specBytes, err := json.Marshal(spec)
if err != nil {
return resp, fmt.Errorf("marshal service spec: %w", err)
}
grpcResp, err := c.GRPCClient.CreateServiceContainer(ctx, &pb.CreateServiceContainerRequest{
ServiceId: serviceID,
ServiceSpec: specBytes,
ContainerName: containerName,
})
if err != nil {
if status.Convert(err).Code() == codes.NotFound {
return resp, errdefs.NotFound(err)
}
return resp, err
}
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
}
return resp, nil
}
// InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID.
func (c *Client) InspectServiceContainer(ctx context.Context, id string) (api.ServiceContainer, error) {
@@ -474,10 +446,13 @@ func (c *Client) InspectServiceContainer(ctx context.Context, id string) (api.Se
type MachineServiceContainers struct {
Metadata *pb.Metadata
Containers []api.ServiceContainer
// HookContainers are one-shot containers for deployment hooks (e.g. pre-deploy).
HookContainers []api.ServiceContainer
}
// ListServiceContainers returns all containers on requested machines that belong to the service with the given
// name or ID. If serviceNameOrID is empty, all service containers are returned.
// Set opts.All to true to include hook containers.
func (c *Client) ListServiceContainers(
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
) ([]MachineServiceContainers, error) {
@@ -501,22 +476,33 @@ func (c *Client) ListServiceContainers(
continue
}
containers := make([]api.ServiceContainer, len(msg.Containers))
for j, sc := range msg.Containers {
if err = json.Unmarshal(sc.Container, &containers[j].Container); err != nil {
return nil, fmt.Errorf("unmarshal container: %w", err)
}
if err = json.Unmarshal(sc.ServiceSpec, &containers[j].ServiceSpec); err != nil {
return nil, fmt.Errorf("unmarshal service spec: %w", err)
}
machineContainers[i].Containers, err = serviceContainersFromProto(msg.Containers)
if err != nil {
return nil, err
}
machineContainers[i].HookContainers, err = serviceContainersFromProto(msg.HookContainers)
if err != nil {
return nil, err
}
machineContainers[i].Containers = containers
}
return machineContainers, nil
}
// serviceContainersFromProto converts a slice of protobuf service containers to api.ServiceContainer.
func serviceContainersFromProto(pbContainers []*pb.ServiceContainer) ([]api.ServiceContainer, error) {
containers := make([]api.ServiceContainer, len(pbContainers))
for i, sc := range pbContainers {
if err := json.Unmarshal(sc.Container, &containers[i].Container); err != nil {
return nil, fmt.Errorf("unmarshal container: %w", err)
}
if err := json.Unmarshal(sc.ServiceSpec, &containers[i].ServiceSpec); err != nil {
return nil, fmt.Errorf("unmarshal service spec: %w", err)
}
}
return containers, nil
}
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
// A service container is a container that has been created with CreateServiceContainer.
func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts container.RemoveOptions) error {
+5 -1
View File
@@ -152,12 +152,16 @@ func (c *Controller) syncContainersToStore(ctx context.Context) error {
return fmt.Errorf("list containers from store: %w", err)
}
containers, err := c.service.ListServiceContainers(ctx, "", container.ListOptions{})
// List containers for all services, including stopped ones and deployment hooks.
result, err := c.service.ListServiceContainers(ctx, "", container.ListOptions{All: true})
if err != nil {
// TODO: mark all containers as outdated in the store.
return fmt.Errorf("list service containers: %w", err)
}
// Sync both regular and one-off hook containers to the store.
containers := append(result.Containers, result.HookContainers...)
// Delete containers from the store that are no longer present in the Docker daemon.
var deleteIDs []string
for _, sc := range storeContainers {
+56 -11
View File
@@ -664,6 +664,39 @@ func (s *Server) CreateServiceContainer(
}
}
// Strip the container configuration that doesn't make sense for the pre-deploy hook and apply its overrides.
if spec.PreDeploy != nil && req.ContainerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
config.Labels = map[string]string{
api.LabelServiceID: req.ServiceId,
api.LabelServiceName: spec.Name,
api.LabelHook: api.LabelHookPreDeploy,
api.LabelManaged: "",
}
config.Healthcheck = &container.HealthConfig{
Test: []string{"NONE"},
}
hostConfig.PortBindings = nil
hostConfig.RestartPolicy = container.RestartPolicy{
Name: container.RestartPolicyDisabled,
}
// Apply the pre-deploy hook overrides.
config.Cmd = spec.PreDeploy.Command
for k, v := range spec.PreDeploy.Env {
envVars[k] = v
}
envVars["UNCLOUD_HOOK_PRE_DEPLOY"] = "true"
config.Env = envVars.ToSlice()
if spec.PreDeploy.Privileged != nil {
hostConfig.Privileged = *spec.PreDeploy.Privileged
}
if spec.PreDeploy.User != "" {
config.User = spec.PreDeploy.User
}
}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
NetworkName: {},
@@ -1009,37 +1042,49 @@ func (s *Server) ListServiceContainers(
}
}
containers, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
result, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// Convert to protobuf format.
pbContainers, err := serviceContainersToProto(result.Containers)
if err != nil {
return nil, err
}
pbHookContainers, err := serviceContainersToProto(result.HookContainers)
if err != nil {
return nil, err
}
return &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Containers: pbContainers,
HookContainers: pbHookContainers,
},
},
}, nil
}
// serviceContainersToProto converts a slice of service containers to protobuf format.
func serviceContainersToProto(containers []api.ServiceContainer) ([]*pb.ServiceContainer, error) {
pbContainers := make([]*pb.ServiceContainer, 0, len(containers))
for _, ctr := range containers {
ctrBytes, err := json.Marshal(ctr.Container)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
}
specBytes, err := json.Marshal(ctr.ServiceSpec)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
}
pbContainers = append(pbContainers, &pb.ServiceContainer{
Container: ctrBytes,
ServiceSpec: specBytes,
})
}
return &pb.ListServiceContainersResponse{
Messages: []*pb.MachineServiceContainers{
{
Containers: pbContainers,
},
},
}, nil
return pbContainers, nil
}
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
+18 -5
View File
@@ -74,11 +74,20 @@ func (s *Service) InspectServiceContainer(ctx context.Context, nameOrID string)
return serviceCtr, nil
}
// ListServiceContainersResult holds the result of listing service containers, split into regular
// service containers and one-off hook containers.
type ListServiceContainersResult struct {
Containers []api.ServiceContainer
HookContainers []api.ServiceContainer
}
// ListServiceContainers lists Docker containers that belong to the service with the given name or ID.
// If serviceIDOrName is empty, all service containers are returned. The opts parameter allows additional filtering.
func (s *Service) ListServiceContainers(
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
) ([]api.ServiceContainer, error) {
) (ListServiceContainersResult, error) {
var result ListServiceContainersResult
if opts.Filters.Len() == 0 {
opts.Filters = filters.NewArgs()
}
@@ -88,10 +97,9 @@ func (s *Service) ListServiceContainers(
containerSummaries, err := s.Client.ContainerList(ctx, opts)
if err != nil {
return nil, err
return result, err
}
var containers []api.ServiceContainer
for _, cs := range containerSummaries {
// Filter by service name or ID if provided.
if serviceNameOrID != "" &&
@@ -106,10 +114,15 @@ func (s *Service) ListServiceContainers(
slog.Error("Failed to inspect service container.", "service", serviceNameOrID, "id", cs.ID, "err", err)
continue
}
containers = append(containers, ctr)
if ctr.IsHook() {
result.HookContainers = append(result.HookContainers, ctr)
} else {
result.Containers = append(result.Containers, ctr)
}
}
return containers, nil
return result, nil
}
// IsContainerdImageStoreEnabled checks if Docker is configured to use the containerd image store: