diff --git a/pkg/api/service.go b/pkg/api/service.go index 873905fe..045851a1 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -55,6 +55,8 @@ type ServiceSpec struct { // Caddy is the optional Caddy reverse proxy configuration for the service. // Caddy and Ports cannot be specified simultaneously. Caddy *CaddySpec `json:",omitempty"` + // Configs is list of configuration objects that can be mounted into the container. + Configs []ConfigSpec // Container defines the desired state of each container in the service. Container ContainerSpec // Mode is the replication mode of the service. Default is ServiceModeReplicated if empty. @@ -67,12 +69,13 @@ type ServiceSpec struct { Ports []PortSpec // Replicas is the number of containers to run for the service. Only valid for a replicated service. Replicas uint `json:",omitempty"` + // StopGracePeriod is how long to wait after SIGTERM before sending SIGKILL when stopping a container. + // Default is 10 seconds if not specified. + StopGracePeriod *time.Duration `json:",omitempty"` // UpdateConfig configures how the service is updated during a deployment. UpdateConfig UpdateConfig `json:",omitempty"` // Volumes is list of data volumes that can be mounted into the container. Volumes []VolumeSpec - // Configs is list of configuration objects that can be mounted into the container. - Configs []ConfigSpec } // CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined. diff --git a/pkg/client/compose/service.go b/pkg/client/compose/service.go index 1f69a461..bf0ffae7 100644 --- a/pkg/client/compose/service.go +++ b/pkg/client/compose/service.go @@ -85,6 +85,11 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser } } + if service.StopGracePeriod != nil { + d := time.Duration(*service.StopGracePeriod) + spec.StopGracePeriod = &d + } + if service.Scale != nil { spec.Replicas = uint(*service.Scale) } diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 07a07b0b..b65ff9fc 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -205,7 +205,8 @@ func TestServiceSpecFromCompose(t *testing.T) { Placement: api.Placement{ Machines: []string{"machine-1", "machine-2"}, }, - Replicas: 3, + Replicas: 3, + StopGracePeriod: api.AsPtr(30 * time.Second), UpdateConfig: api.UpdateConfig{ Order: api.UpdateOrderStopFirst, MonitorPeriod: &api.DefaultHealthMonitorPeriod, diff --git a/pkg/client/compose/testdata/compose-full-spec.yaml b/pkg/client/compose/testdata/compose-full-spec.yaml index 989207a1..13bd83e6 100644 --- a/pkg/client/compose/testdata/compose-full-spec.yaml +++ b/pkg/client/compose/testdata/compose-full-spec.yaml @@ -37,6 +37,7 @@ services: privileged: true pull_policy: always scale: 3 + stop_grace_period: 30s sysctls: - net.ipv4.ip_forward=1 ulimits: diff --git a/pkg/client/deploy/operation/container.go b/pkg/client/deploy/operation/container.go index 12d08970..f7e1c6a3 100644 --- a/pkg/client/deploy/operation/container.go +++ b/pkg/client/deploy/operation/container.go @@ -3,6 +3,7 @@ package operation import ( "context" "fmt" + "time" "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" @@ -53,13 +54,14 @@ func (o *RunContainerOperation) String() string { // StopContainerOperation stops a container on a specific machine. type StopContainerOperation struct { - ServiceID string - ContainerID string - MachineID string + ServiceID string + ContainerID string + MachineID string + StopGracePeriod *time.Duration } func (o *StopContainerOperation) Execute(ctx context.Context, cli Client) error { - if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil { + if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, stopOptions(o.StopGracePeriod)); err != nil { return fmt.Errorf("stop container: %w", err) } return nil @@ -78,15 +80,18 @@ func (o *StopContainerOperation) String() string { // RemoveContainerOperation stops and removes a container from a specific machine. type RemoveContainerOperation struct { - MachineID string - Container api.ServiceContainer + MachineID string + Container api.ServiceContainer + StopGracePeriod *time.Duration } func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) error { - if err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.StopOptions{}); err != nil { + err := cli.StopContainer(ctx, o.Container.ServiceID(), o.Container.ID, stopOptions(o.StopGracePeriod)) + if err != nil { return fmt.Errorf("stop container: %w", err) } - if err := cli.RemoveContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.RemoveOptions{ + + if err = cli.RemoveContainer(ctx, o.Container.ServiceID(), o.Container.ID, container.RemoveOptions{ // Remove anonymous volumes created by the container. RemoveVolumes: true, }); err != nil { @@ -119,6 +124,7 @@ type ReplaceContainerOperation struct { Order string // SkipHealthMonitor skips the monitoring period and health checks after starting a new container. SkipHealthMonitor bool + StopGracePeriod *time.Duration } func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) error { @@ -133,7 +139,8 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err } wasRunning = ctr.Container.State.Running if wasRunning { - if err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, container.StopOptions{}); err != nil { + err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, stopOptions(o.StopGracePeriod)) + if err != nil { return fmt.Errorf("stop old container: %w", err) } } @@ -156,7 +163,7 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err // Use context without progress to not overwrite the container Unhealthy status with Stopped. ctxWithoutProgress := progress.WithContextWriter(ctx, nil) - _ = cli.StopContainer(ctxWithoutProgress, o.ServiceID, resp.ID, container.StopOptions{}) + _ = cli.StopContainer(ctxWithoutProgress, o.ServiceID, resp.ID, stopOptions(o.StopGracePeriod)) newCtr := fmt.Sprintf("%s/%s", o.Spec.Name, stringid.TruncateID(resp.ID)) healthErr := fmt.Errorf( @@ -186,12 +193,12 @@ func (o *ReplaceContainerOperation) Execute(ctx context.Context, cli Client) err // There still might be a brief downtime (for a 1 replica service) when Caddy doesn't know about // the new container but we're stopping the old container. We should somehow ensure Caddy is updated // with the new container before we stop the old one to avoid this downtime. - if err := cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, container.StopOptions{}); err != nil { + if err = cli.StopContainer(ctx, o.ServiceID, o.OldContainer.ID, stopOptions(o.StopGracePeriod)); err != nil { return fmt.Errorf("stop old container: %w", err) } } - if err := cli.RemoveContainer(ctx, o.ServiceID, o.OldContainer.ID, container.RemoveOptions{ + if err = cli.RemoveContainer(ctx, o.ServiceID, o.OldContainer.ID, container.RemoveOptions{ RemoveVolumes: true, }); err != nil { return fmt.Errorf("remove old container: %w", err) @@ -209,3 +216,12 @@ func (o *ReplaceContainerOperation) String() string { return fmt.Sprintf("ReplaceContainerOperation[machine_id=%s service_id=%s old_container_id=%s order=%s]", o.MachineID, o.ServiceID, o.OldContainer.ID, o.Order) } + +// stopOptions converts a stop grace period duration to Docker container stop options. +func stopOptions(gracePeriod *time.Duration) container.StopOptions { + if gracePeriod == nil { + return container.StopOptions{} + } + t := int(gracePeriod.Seconds()) + return container.StopOptions{Timeout: &t} +} diff --git a/pkg/client/deploy/strategy.go b/pkg/client/deploy/strategy.go index f02044e1..f283aa4c 100644 --- a/pkg/client/deploy/strategy.go +++ b/pkg/client/deploy/strategy.go @@ -175,6 +175,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) OldContainer: ctr, Order: order, SkipHealthMonitor: s.SkipHealthMonitor, + StopGracePeriod: spec.StopGracePeriod, }) } @@ -182,8 +183,9 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) for mid, containers := range containersOnMachine { for _, c := range containers { plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ - MachineID: mid, - Container: c, + MachineID: mid, + Container: c, + StopGracePeriod: spec.StopGracePeriod, }) } } @@ -234,8 +236,9 @@ func (s *RollingStrategy) planGlobal(svc *api.Service, spec api.ServiceSpec) (Pl for _, containers := range containersOnMachine { for _, c := range containers { plan.Operations = append(plan.Operations, &operation.RemoveContainerOperation{ - MachineID: c.MachineID, - Container: c.Container, + MachineID: c.MachineID, + Container: c.Container, + StopGracePeriod: spec.StopGracePeriod, }) } } @@ -286,8 +289,9 @@ func reconcileGlobalContainer( continue } ops = append(ops, &operation.RemoveContainerOperation{ - MachineID: old.MachineID, - Container: old.Container, + MachineID: old.MachineID, + Container: old.Container, + StopGracePeriod: spec.StopGracePeriod, }) } break @@ -319,9 +323,10 @@ func reconcileGlobalContainer( conflictingPorts, err := c.Container.ConflictingServicePorts(spec.Ports) if err != nil || len(conflictingPorts) > 0 { ops = append(ops, &operation.StopContainerOperation{ - ServiceID: serviceID, - ContainerID: c.Container.ID, - MachineID: machineID, + ServiceID: serviceID, + ContainerID: c.Container.ID, + MachineID: machineID, + StopGracePeriod: spec.StopGracePeriod, }) } } @@ -335,6 +340,7 @@ func reconcileGlobalContainer( OldContainer: containerToReplace.Container, Order: order, SkipHealthMonitor: skipHealthCheck, + StopGracePeriod: spec.StopGracePeriod, }) // Remove any other containers (there shouldn't be any in normal operation). @@ -343,8 +349,9 @@ func reconcileGlobalContainer( continue } ops = append(ops, &operation.RemoveContainerOperation{ - MachineID: c.MachineID, - Container: c.Container, + MachineID: c.MachineID, + Container: c.Container, + StopGracePeriod: spec.StopGracePeriod, }) } } else { @@ -357,8 +364,9 @@ func reconcileGlobalContainer( }) for _, c := range containers { ops = append(ops, &operation.RemoveContainerOperation{ - MachineID: c.MachineID, - Container: c.Container, + MachineID: c.MachineID, + Container: c.Container, + StopGracePeriod: spec.StopGracePeriod, }) } } diff --git a/website/docs/8-compose-file-reference/1-support-matrix.md b/website/docs/8-compose-file-reference/1-support-matrix.md index c1127dd4..4fe27c48 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -3,68 +3,69 @@ Uncloud supports a subset of the [Compose specification](https://compose-spec.io/) with some extensions and limitations. The following table shows the support status for main Compose features: -| Feature | Support Status | Notes | -|--------------------|--------------------|----------------------------------------------------------------------------------------------------------------| -| **Services** | | | -| `build` | ✅ Supported | Build context and Dockerfile | -| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) | -| `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop | -| `command` | ✅ Supported | Override container command | -| `configs` | ✅ Supported | File-based and inline configs | -| `cpus` | ✅ Supported | CPU limit | -| `depends_on` | ⚠️ Limited | Services deployed in order but conditions not checked | -| `devices` | ✅ Supported | Device mappings | -| `dns` | ❌ Not supported | Built-in service discovery | -| `dns_search` | ❌ Not supported | Built-in service discovery | -| `entrypoint` | ✅ Supported | Override container entrypoint | -| `env_file` | ✅ Supported | Environment file | -| `environment` | ✅ Supported | Environment variables | -| `gpus` | ✅ Supported | GPU device access | -| `healthcheck` | ✅ Supported | Health check configuration | -| `image` | ✅ Supported | Container image specification | -| `init` | ✅ Supported | Run init process in container | -| `labels` | ❌ Not supported | | -| `links` | ❌ Not supported | Use service names for communication | -| `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver | -| `mem_limit` | ✅ Supported | Memory limit | -| `mem_reservation` | ✅ Supported | Memory reservation | -| `mem_swappiness` | ❌ Not supported | | -| `memswap_limit` | ❌ Not supported | | -| `networks` | ❌ Not supported | All containers share cluster network | -| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](#x-ports) for HTTP/HTTPS | -| `privileged` | ✅ Supported | Run containers in privileged mode | -| `pull_policy` | ✅ Supported | `always`, `missing`, `never` | -| `secrets` | ❌ Not supported | Use configs or environment variables | -| `security_opt` | ❌ Not supported | | -| `storage_opt` | ❌ Not supported | | -| `sysctls` | ✅ Supported | Namespaced kernel parameters | -| `user` | ✅ Supported | Set container user | -| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | -| **Deploy** | | | -| `labels` | ❌ Not supported | | -| `mode` | ✅ Supported | Either `global` or `replicated` | -| `placement` | ❌ Not supported | Use [`x-machines`](#x-machines) extension | -| `replicas` | ✅ Supported | Number of container replicas | -| `resources` | ⚠️ Limited | CPU, memory limits and device reservations | -| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` | -| `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) | -| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) | -| **Volumes** | | | -| Named volumes | ✅ Supported | Docker volumes | -| Bind mounts | ✅ Supported | Host path binding | -| Tmpfs mounts | ✅ Supported | In-memory filesystems | -| Volume labels | ✅ Supported | Custom labels | -| External volumes | ✅ Supported | Must exist before deployment | -| Volume drivers | ⚠️ Limited | Local driver only | -| **Configs** | | | -| File-based configs | ✅ Supported | Read from file | -| Inline configs | ✅ Supported | Defined in compose file | -| External configs | ❌ Not supported | Not supported | -| Short syntax | ❌ Not supported | Use long syntax only | -| **Extensions** | | | -| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | -| `x-machines` | ✅ Uncloud-specific | Machine placement constraints | -| `x-ports` | ✅ Uncloud-specific | Service port publishing | +| Feature | Support Status | Notes | +|---------------------|--------------------|----------------------------------------------------------------------------------------------------------------| +| **Services** | | | +| `build` | ✅ Supported | Build context and Dockerfile | +| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) | +| `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop | +| `command` | ✅ Supported | Override container command | +| `configs` | ✅ Supported | File-based and inline configs | +| `cpus` | ✅ Supported | CPU limit | +| `depends_on` | ⚠️ Limited | Services deployed in order but conditions not checked | +| `devices` | ✅ Supported | Device mappings | +| `dns` | ❌ Not supported | Built-in service discovery | +| `dns_search` | ❌ Not supported | Built-in service discovery | +| `entrypoint` | ✅ Supported | Override container entrypoint | +| `env_file` | ✅ Supported | Environment file | +| `environment` | ✅ Supported | Environment variables | +| `gpus` | ✅ Supported | GPU device access | +| `healthcheck` | ✅ Supported | Health check configuration | +| `image` | ✅ Supported | Container image specification | +| `init` | ✅ Supported | Run init process in container | +| `labels` | ❌ Not supported | | +| `links` | ❌ Not supported | Use service names for communication | +| `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver | +| `mem_limit` | ✅ Supported | Memory limit | +| `mem_reservation` | ✅ Supported | Memory reservation | +| `mem_swappiness` | ❌ Not supported | | +| `memswap_limit` | ❌ Not supported | | +| `networks` | ❌ Not supported | All containers share cluster network | +| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](#x-ports) for HTTP/HTTPS | +| `privileged` | ✅ Supported | Run containers in privileged mode | +| `pull_policy` | ✅ Supported | `always`, `missing`, `never` | +| `secrets` | ❌ Not supported | Use configs or environment variables | +| `security_opt` | ❌ Not supported | | +| `stop_grace_period` | ✅ Supported | Time to wait after SIGTERM before SIGKILL | +| `storage_opt` | ❌ Not supported | | +| `sysctls` | ✅ Supported | Namespaced kernel parameters | +| `user` | ✅ Supported | Set container user | +| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | +| **Deploy** | | | +| `labels` | ❌ Not supported | | +| `mode` | ✅ Supported | Either `global` or `replicated` | +| `placement` | ❌ Not supported | Use [`x-machines`](#x-machines) extension | +| `replicas` | ✅ Supported | Number of container replicas | +| `resources` | ⚠️ Limited | CPU, memory limits and device reservations | +| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` | +| `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) | +| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) | +| **Volumes** | | | +| Named volumes | ✅ Supported | Docker volumes | +| Bind mounts | ✅ Supported | Host path binding | +| Tmpfs mounts | ✅ Supported | In-memory filesystems | +| Volume labels | ✅ Supported | Custom labels | +| External volumes | ✅ Supported | Must exist before deployment | +| Volume drivers | ⚠️ Limited | Local driver only | +| **Configs** | | | +| File-based configs | ✅ Supported | Read from file | +| Inline configs | ✅ Supported | Defined in compose file | +| External configs | ❌ Not supported | Not supported | +| Short syntax | ❌ Not supported | Use long syntax only | +| **Extensions** | | | +| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | +| `x-machines` | ✅ Uncloud-specific | Machine placement constraints | +| `x-ports` | ✅ Uncloud-specific | Service port publishing | ### Legend