Skip to content

Commit 1c8b770

Browse files
committed
feat(pre-deploy): add support for pre-deploy hook containers in service management, sync *all* containers to store
1 parent cef0472 commit 1c8b770

11 files changed

Lines changed: 509 additions & 299 deletions

File tree

‎internal/machine/api/pb/docker.pb.go‎

Lines changed: 300 additions & 216 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎internal/machine/api/pb/docker.proto‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ message CreateServiceContainerRequest {
228228
// JSON serialised api.ServiceSpec.
229229
bytes service_spec = 2;
230230
string container_name = 3;
231+
232+
// ContainerType specifies the purpose of the container being created.
233+
enum ContainerType {
234+
// SERVICE is the default type for long-running service containers.
235+
SERVICE = 0;
236+
// PRE_DEPLOY is a one-shot container for a pre-deploy hook to run before service deployment.
237+
PRE_DEPLOY = 1;
238+
}
239+
ContainerType container_type = 4;
231240
}
232241

233242
message ServiceContainer {
@@ -252,4 +261,6 @@ message ListServiceContainersResponse {
252261
message MachineServiceContainers {
253262
Metadata metadata = 1;
254263
repeated ServiceContainer containers = 2;
264+
// One-shot containers for deployment hooks (e.g. pre-deploy).
265+
repeated ServiceContainer hook_containers = 3;
255266
}

‎internal/machine/docker/client.go‎

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -420,34 +420,6 @@ func (c *Client) RemoveVolume(ctx context.Context, id string, force bool) error
420420
return err
421421
}
422422

423-
// CreateServiceContainer creates a new container for the service with the given specifications.
424-
func (c *Client) CreateServiceContainer(
425-
ctx context.Context, serviceID string, spec api.ServiceSpec, containerName string,
426-
) (container.CreateResponse, error) {
427-
var resp container.CreateResponse
428-
429-
specBytes, err := json.Marshal(spec)
430-
if err != nil {
431-
return resp, fmt.Errorf("marshal service spec: %w", err)
432-
}
433-
grpcResp, err := c.GRPCClient.CreateServiceContainer(ctx, &pb.CreateServiceContainerRequest{
434-
ServiceId: serviceID,
435-
ServiceSpec: specBytes,
436-
ContainerName: containerName,
437-
})
438-
if err != nil {
439-
if status.Convert(err).Code() == codes.NotFound {
440-
return resp, errdefs.NotFound(err)
441-
}
442-
return resp, err
443-
}
444-
445-
if err = json.Unmarshal(grpcResp.Response, &resp); err != nil {
446-
return resp, fmt.Errorf("unmarshal gRPC response: %w", err)
447-
}
448-
return resp, nil
449-
}
450-
451423
// InspectServiceContainer returns the container information and service specification that was used to create the
452424
// container with the given ID.
453425
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
474446
type MachineServiceContainers struct {
475447
Metadata *pb.Metadata
476448
Containers []api.ServiceContainer
449+
// HookContainers are one-shot containers for deployment hooks (e.g. pre-deploy).
450+
HookContainers []api.ServiceContainer
477451
}
478452

479453
// ListServiceContainers returns all containers on requested machines that belong to the service with the given
480454
// name or ID. If serviceNameOrID is empty, all service containers are returned.
455+
// Set opts.All to true to include hook containers.
481456
func (c *Client) ListServiceContainers(
482457
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
483458
) ([]MachineServiceContainers, error) {
@@ -501,22 +476,33 @@ func (c *Client) ListServiceContainers(
501476
continue
502477
}
503478

504-
containers := make([]api.ServiceContainer, len(msg.Containers))
505-
for j, sc := range msg.Containers {
506-
if err = json.Unmarshal(sc.Container, &containers[j].Container); err != nil {
507-
return nil, fmt.Errorf("unmarshal container: %w", err)
508-
}
509-
if err = json.Unmarshal(sc.ServiceSpec, &containers[j].ServiceSpec); err != nil {
510-
return nil, fmt.Errorf("unmarshal service spec: %w", err)
511-
}
479+
machineContainers[i].Containers, err = serviceContainersFromProto(msg.Containers)
480+
if err != nil {
481+
return nil, err
482+
}
483+
machineContainers[i].HookContainers, err = serviceContainersFromProto(msg.HookContainers)
484+
if err != nil {
485+
return nil, err
512486
}
513-
514-
machineContainers[i].Containers = containers
515487
}
516488

517489
return machineContainers, nil
518490
}
519491

492+
// serviceContainersFromProto converts a slice of protobuf service containers to api.ServiceContainer.
493+
func serviceContainersFromProto(pbContainers []*pb.ServiceContainer) ([]api.ServiceContainer, error) {
494+
containers := make([]api.ServiceContainer, len(pbContainers))
495+
for i, sc := range pbContainers {
496+
if err := json.Unmarshal(sc.Container, &containers[i].Container); err != nil {
497+
return nil, fmt.Errorf("unmarshal container: %w", err)
498+
}
499+
if err := json.Unmarshal(sc.ServiceSpec, &containers[i].ServiceSpec); err != nil {
500+
return nil, fmt.Errorf("unmarshal service spec: %w", err)
501+
}
502+
}
503+
return containers, nil
504+
}
505+
520506
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
521507
// A service container is a container that has been created with CreateServiceContainer.
522508
func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts container.RemoveOptions) error {

‎internal/machine/docker/controller.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,16 @@ func (c *Controller) syncContainersToStore(ctx context.Context) error {
152152
return fmt.Errorf("list containers from store: %w", err)
153153
}
154154

155-
containers, err := c.service.ListServiceContainers(ctx, "", container.ListOptions{})
155+
// List containers for all services, including stopped ones and deployment hooks.
156+
result, err := c.service.ListServiceContainers(ctx, "", container.ListOptions{All: true})
156157
if err != nil {
157158
// TODO: mark all containers as outdated in the store.
158159
return fmt.Errorf("list service containers: %w", err)
159160
}
160161

162+
// Sync both regular and one-off hook containers to the store.
163+
containers := append(result.Containers, result.HookContainers...)
164+
161165
// Delete containers from the store that are no longer present in the Docker daemon.
162166
var deleteIDs []string
163167
for _, sc := range storeContainers {

‎internal/machine/docker/server.go‎

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,39 @@ func (s *Server) CreateServiceContainer(
664664
}
665665
}
666666

667+
// Strip the container configuration that doesn't make sense for the pre-deploy hook and apply its overrides.
668+
if spec.PreDeploy != nil && req.ContainerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
669+
config.Labels = map[string]string{
670+
api.LabelServiceID: req.ServiceId,
671+
api.LabelServiceName: spec.Name,
672+
api.LabelHook: api.LabelHookPreDeploy,
673+
api.LabelManaged: "",
674+
}
675+
config.Healthcheck = &container.HealthConfig{
676+
Test: []string{"NONE"},
677+
}
678+
hostConfig.PortBindings = nil
679+
hostConfig.RestartPolicy = container.RestartPolicy{
680+
Name: container.RestartPolicyDisabled,
681+
}
682+
683+
// Apply the pre-deploy hook overrides.
684+
config.Cmd = spec.PreDeploy.Command
685+
686+
for k, v := range spec.PreDeploy.Env {
687+
envVars[k] = v
688+
}
689+
envVars["UNCLOUD_HOOK_PRE_DEPLOY"] = "true"
690+
config.Env = envVars.ToSlice()
691+
692+
if spec.PreDeploy.Privileged != nil {
693+
hostConfig.Privileged = *spec.PreDeploy.Privileged
694+
}
695+
if spec.PreDeploy.User != "" {
696+
config.User = spec.PreDeploy.User
697+
}
698+
}
699+
667700
networkConfig := &network.NetworkingConfig{
668701
EndpointsConfig: map[string]*network.EndpointSettings{
669702
NetworkName: {},
@@ -1009,37 +1042,49 @@ func (s *Server) ListServiceContainers(
10091042
}
10101043
}
10111044

1012-
containers, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
1045+
result, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
10131046
if err != nil {
10141047
return nil, status.Error(codes.Internal, err.Error())
10151048
}
10161049

1017-
// Convert to protobuf format.
1050+
pbContainers, err := serviceContainersToProto(result.Containers)
1051+
if err != nil {
1052+
return nil, err
1053+
}
1054+
pbHookContainers, err := serviceContainersToProto(result.HookContainers)
1055+
if err != nil {
1056+
return nil, err
1057+
}
1058+
1059+
return &pb.ListServiceContainersResponse{
1060+
Messages: []*pb.MachineServiceContainers{
1061+
{
1062+
Containers: pbContainers,
1063+
HookContainers: pbHookContainers,
1064+
},
1065+
},
1066+
}, nil
1067+
}
1068+
1069+
// serviceContainersToProto converts a slice of service containers to protobuf format.
1070+
func serviceContainersToProto(containers []api.ServiceContainer) ([]*pb.ServiceContainer, error) {
10181071
pbContainers := make([]*pb.ServiceContainer, 0, len(containers))
10191072
for _, ctr := range containers {
10201073
ctrBytes, err := json.Marshal(ctr.Container)
10211074
if err != nil {
10221075
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
10231076
}
1024-
10251077
specBytes, err := json.Marshal(ctr.ServiceSpec)
10261078
if err != nil {
10271079
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
10281080
}
1029-
10301081
pbContainers = append(pbContainers, &pb.ServiceContainer{
10311082
Container: ctrBytes,
10321083
ServiceSpec: specBytes,
10331084
})
10341085
}
10351086

1036-
return &pb.ListServiceContainersResponse{
1037-
Messages: []*pb.MachineServiceContainers{
1038-
{
1039-
Containers: pbContainers,
1040-
},
1041-
},
1042-
}, nil
1087+
return pbContainers, nil
10431088
}
10441089

10451090
// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.

‎internal/machine/docker/service.go‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,20 @@ func (s *Service) InspectServiceContainer(ctx context.Context, nameOrID string)
7474
return serviceCtr, nil
7575
}
7676

77+
// ListServiceContainersResult holds the result of listing service containers, split into regular
78+
// service containers and one-off hook containers.
79+
type ListServiceContainersResult struct {
80+
Containers []api.ServiceContainer
81+
HookContainers []api.ServiceContainer
82+
}
83+
7784
// ListServiceContainers lists Docker containers that belong to the service with the given name or ID.
7885
// If serviceIDOrName is empty, all service containers are returned. The opts parameter allows additional filtering.
7986
func (s *Service) ListServiceContainers(
8087
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
81-
) ([]api.ServiceContainer, error) {
88+
) (ListServiceContainersResult, error) {
89+
var result ListServiceContainersResult
90+
8291
if opts.Filters.Len() == 0 {
8392
opts.Filters = filters.NewArgs()
8493
}
@@ -88,10 +97,9 @@ func (s *Service) ListServiceContainers(
8897

8998
containerSummaries, err := s.Client.ContainerList(ctx, opts)
9099
if err != nil {
91-
return nil, err
100+
return result, err
92101
}
93102

94-
var containers []api.ServiceContainer
95103
for _, cs := range containerSummaries {
96104
// Filter by service name or ID if provided.
97105
if serviceNameOrID != "" &&
@@ -106,10 +114,15 @@ func (s *Service) ListServiceContainers(
106114
slog.Error("Failed to inspect service container.", "service", serviceNameOrID, "id", cs.ID, "err", err)
107115
continue
108116
}
109-
containers = append(containers, ctr)
117+
118+
if ctr.IsHook() {
119+
result.HookContainers = append(result.HookContainers, ctr)
120+
} else {
121+
result.Containers = append(result.Containers, ctr)
122+
}
110123
}
111124

112-
return containers, nil
125+
return result, nil
113126
}
114127

115128
// IsContainerdImageStoreEnabled checks if Docker is configured to use the containerd image store:

‎pkg/api/client.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ type ContainerClient interface {
2121
CreateContainer(
2222
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
2323
) (container.CreateResponse, error)
24+
CreatePreDeployHookContainer(
25+
ctx context.Context, serviceID string, spec ServiceSpec, machineID string,
26+
) (container.CreateResponse, error)
2427
ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error)
2528
InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error)
2629
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error

‎pkg/api/container.go‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ const (
1818
// DockerNetworkName is the name of the Docker network used by uncloud. Keep the value in sync with NetworkName
1919
// in internal/machine/docker/manager.go.
2020
DockerNetworkName = "uncloud"
21+
2122
LabelManaged = "uncloud.managed"
2223
LabelServiceID = "uncloud.service.id"
2324
LabelServiceName = "uncloud.service.name"
2425
LabelServiceMode = "uncloud.service.mode"
2526
LabelServicePorts = "uncloud.service.ports"
27+
// LabelHook marks a container as a deployment hook. The value indicates the hook type (e.g. LabelHookPreDeploy).
28+
LabelHook = "uncloud.service.hook"
29+
// LabelHookPreDeploy indicates that the container is a pre-deploy hook that runs before deploying the service.
30+
LabelHookPreDeploy = "pre-deploy"
2631
)
2732

2833
type Container struct {
@@ -181,10 +186,17 @@ func (c *ServiceContainer) ServiceName() string {
181186

182187
// ServiceMode returns the replication mode of the service this container belongs to.
183188
func (c *ServiceContainer) ServiceMode() string {
184-
return c.Config.Labels[LabelServiceMode]
189+
return c.ServiceSpec.Mode
190+
}
191+
192+
// IsHook returns true if the container is a deployment hook (e.g. pre-deploy).
193+
func (c *ServiceContainer) IsHook() bool {
194+
_, ok := c.Config.Labels[LabelHook]
195+
return ok
185196
}
186197

187198
// ServicePorts returns the ports this container publishes as part of its service.
199+
// TODO: return ports from ServiceSpec to allow updating ingress ports without recreating containers.
188200
func (c *ServiceContainer) ServicePorts() ([]PortSpec, error) {
189201
encoded, ok := c.Config.Labels[LabelServicePorts]
190202
if !ok {

‎pkg/api/service.go‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,8 @@ type PreDeployHook struct {
454454
Command []string
455455
// Env defines additional environment variables for the container, merged with the service's environment variables.
456456
Env EnvVars `json:",omitempty"`
457-
// Privileged gives extended privileges to the container.
458-
Privileged bool `json:",omitempty"`
457+
// Privileged overrides the container's privileged mode. nil means inherit from the service.
458+
Privileged *bool `json:",omitempty"`
459459
// Timeout is the maximum duration to wait for the command to complete. On timeout, the container is stopped
460460
// and the deployment fails. If nil, a default timeout is used.
461461
Timeout *time.Duration `json:",omitempty"`
@@ -505,10 +505,13 @@ type RunServiceResponse struct {
505505
}
506506

507507
type Service struct {
508-
ID string
509-
Name string
510-
Mode string
508+
ID string
509+
Name string
510+
Mode string
511+
// Containers is the regular long-running service containers.
511512
Containers []MachineServiceContainer
513+
// HookContainers are one-shot containers for deployment hooks (e.g. pre-deploy).
514+
HookContainers []MachineServiceContainer
512515
}
513516

514517
type MachineServiceContainer struct {

0 commit comments

Comments
 (0)