mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
feat: add support for healthcheck in Compose
This commit is contained in:
@@ -567,6 +567,22 @@ func (s *Server) CreateServiceContainer(
|
||||
if spec.Mode == "" {
|
||||
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
||||
}
|
||||
if hc := spec.Container.Healthcheck; hc != nil {
|
||||
if hc.Disable {
|
||||
config.Healthcheck = &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
}
|
||||
} else {
|
||||
config.Healthcheck = &container.HealthConfig{
|
||||
Test: hc.Test,
|
||||
Interval: hc.Interval,
|
||||
Timeout: hc.Timeout,
|
||||
StartPeriod: hc.StartPeriod,
|
||||
StartInterval: hc.StartInterval,
|
||||
Retries: int(hc.Retries),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
|
||||
var err error
|
||||
|
||||
+46
-16
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/distribution/reference"
|
||||
@@ -74,14 +75,6 @@ type ServiceSpec struct {
|
||||
Configs []ConfigSpec
|
||||
}
|
||||
|
||||
// UpdateConfig configures how a service is updated during a deployment.
|
||||
type UpdateConfig struct {
|
||||
// Order specifies the order of operations during an update.
|
||||
// Valid values are "start-first" (default for stateless services) and "stop-first" (default for services with volumes).
|
||||
// Empty value means the strategy will determine the order based on service characteristics.
|
||||
Order string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined.
|
||||
func (s *ServiceSpec) CaddyConfig() string {
|
||||
if s.Caddy == nil {
|
||||
@@ -251,8 +244,11 @@ type ContainerSpec struct {
|
||||
// Entrypoint overrides the default ENTRYPOINT of the image.
|
||||
Entrypoint []string
|
||||
// Env defines the environment variables to set inside the container.
|
||||
Env EnvVars
|
||||
Image string
|
||||
Env EnvVars
|
||||
// Healthcheck defines the health check configuration for the container or overrides the health check options
|
||||
// defined in the image. If nil, the image's default health check is used.
|
||||
Healthcheck *HealthcheckSpec `json:",omitempty"`
|
||||
Image string
|
||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||
Init *bool
|
||||
// LogDriver overrides the default logging driver for the container. Each Docker daemon can have its own default.
|
||||
@@ -347,6 +343,17 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
||||
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
||||
copy(spec.Entrypoint, s.Entrypoint)
|
||||
}
|
||||
if s.Env != nil {
|
||||
spec.Env = make(EnvVars, len(s.Env))
|
||||
for k, v := range s.Env {
|
||||
spec.Env[k] = v
|
||||
}
|
||||
}
|
||||
if s.Healthcheck != nil {
|
||||
hc := *s.Healthcheck
|
||||
hc.Test = slices.Clone(s.Healthcheck.Test)
|
||||
spec.Healthcheck = &hc
|
||||
}
|
||||
if s.LogDriver != nil {
|
||||
logDriver := *s.LogDriver
|
||||
if s.LogDriver.Options != nil {
|
||||
@@ -354,12 +361,6 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
||||
}
|
||||
spec.LogDriver = &logDriver
|
||||
}
|
||||
if s.Env != nil {
|
||||
spec.Env = make(EnvVars, len(s.Env))
|
||||
for k, v := range s.Env {
|
||||
spec.Env[k] = v
|
||||
}
|
||||
}
|
||||
if s.Volumes != nil {
|
||||
spec.Volumes = make([]string, len(s.Volumes))
|
||||
copy(spec.Volumes, s.Volumes)
|
||||
@@ -389,6 +390,7 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
||||
if s.Resources.DeviceReservations != nil {
|
||||
spec.Resources.DeviceReservations = slices.Clone(s.Resources.DeviceReservations)
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
@@ -406,6 +408,26 @@ func (e EnvVars) ToSlice() []string {
|
||||
return env
|
||||
}
|
||||
|
||||
// HealthcheckSpec defines the health check configuration for a container.
|
||||
type HealthcheckSpec struct {
|
||||
// Test is the command used to check health.
|
||||
// Formats: ["CMD", args...], ["CMD-SHELL", "command"], or ["NONE"] to disable.
|
||||
Test []string `json:",omitempty"`
|
||||
// Interval is the time between health checks.
|
||||
// Zero means to inherit the value from the image or use the Docker default (30s) if not defined in the image.
|
||||
Interval time.Duration `json:",omitempty"`
|
||||
// Timeout is how long to wait before considering the checck to have hung.
|
||||
Timeout time.Duration `json:",omitempty"`
|
||||
// StartPeriod is the initialisation time for a container before the retries start to count down.
|
||||
StartPeriod time.Duration `json:",omitempty"`
|
||||
// StartInterval is the time between health checks during the start period.
|
||||
StartInterval time.Duration `json:",omitempty"`
|
||||
// Retries is the number of consecutive failures needed to consider a container unhealthy.
|
||||
Retries uint `json:",omitempty"`
|
||||
// Disable disables the health check defined in the image. true is equivalent to setting Test to ["NONE"].
|
||||
Disable bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type LogDriver struct {
|
||||
// Name of the logging driver to use.
|
||||
Name string
|
||||
@@ -413,6 +435,14 @@ type LogDriver struct {
|
||||
Options map[string]string
|
||||
}
|
||||
|
||||
// UpdateConfig configures how a service is updated during a deployment.
|
||||
type UpdateConfig struct {
|
||||
// Order specifies the order of operations during an update.
|
||||
// Valid values are "start-first" (default for stateless services) and "stop-first" (default for services with volumes).
|
||||
// Empty value means the strategy will determine the order based on service characteristics.
|
||||
Order string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type RunServiceResponse struct {
|
||||
ID string
|
||||
Name string
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -44,18 +45,19 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
||||
|
||||
spec := api.ServiceSpec{
|
||||
Container: api.ContainerSpec{
|
||||
CapAdd: service.CapAdd,
|
||||
CapDrop: service.CapDrop,
|
||||
Command: service.Command,
|
||||
Entrypoint: service.Entrypoint,
|
||||
Env: env,
|
||||
Image: service.Image,
|
||||
Init: service.Init,
|
||||
Privileged: service.Privileged,
|
||||
PullPolicy: pullPolicy,
|
||||
Resources: resourcesFromCompose(service),
|
||||
Sysctls: service.Sysctls,
|
||||
User: service.User,
|
||||
CapAdd: service.CapAdd,
|
||||
CapDrop: service.CapDrop,
|
||||
Command: service.Command,
|
||||
Entrypoint: service.Entrypoint,
|
||||
Env: env,
|
||||
Healthcheck: healthcheckFromCompose(service.HealthCheck),
|
||||
Image: service.Image,
|
||||
Init: service.Init,
|
||||
Privileged: service.Privileged,
|
||||
PullPolicy: pullPolicy,
|
||||
Resources: resourcesFromCompose(service),
|
||||
Sysctls: service.Sysctls,
|
||||
User: service.User,
|
||||
},
|
||||
Name: serviceName,
|
||||
Mode: api.ServiceModeReplicated,
|
||||
@@ -135,6 +137,34 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func healthcheckFromCompose(hc *types.HealthCheckConfig) *api.HealthcheckSpec {
|
||||
if hc == nil {
|
||||
return nil
|
||||
}
|
||||
if hc.Disable {
|
||||
return &api.HealthcheckSpec{Disable: true}
|
||||
}
|
||||
|
||||
spec := &api.HealthcheckSpec{Test: hc.Test}
|
||||
if hc.Interval != nil {
|
||||
spec.Interval = time.Duration(*hc.Interval)
|
||||
}
|
||||
if hc.Timeout != nil {
|
||||
spec.Timeout = time.Duration(*hc.Timeout)
|
||||
}
|
||||
if hc.StartPeriod != nil {
|
||||
spec.StartPeriod = time.Duration(*hc.StartPeriod)
|
||||
}
|
||||
if hc.StartInterval != nil {
|
||||
spec.StartInterval = time.Duration(*hc.StartInterval)
|
||||
}
|
||||
if hc.Retries != nil {
|
||||
spec.Retries = uint(*hc.Retries)
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||
resources := api.ContainerResources{
|
||||
CPU: int64(service.CPUS * 1e9),
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
@@ -109,6 +110,14 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
||||
"EMPTY": "",
|
||||
"VAR": "value",
|
||||
},
|
||||
Healthcheck: &api.HealthcheckSpec{
|
||||
Test: []string{"CMD", "curl", "-f", "http://localhost"},
|
||||
Interval: 1*time.Minute + 30*time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
Retries: 5,
|
||||
StartPeriod: 15 * time.Second,
|
||||
StartInterval: 2 * time.Second,
|
||||
},
|
||||
Image: "nginx:latest",
|
||||
Init: &initTrue,
|
||||
LogDriver: &api.LogDriver{
|
||||
|
||||
@@ -11,6 +11,13 @@ services:
|
||||
BOOL: "true"
|
||||
EMPTY: ""
|
||||
VAR: value
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||
interval: 1m30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
start_interval: 2s
|
||||
image: nginx:latest
|
||||
init: true
|
||||
logging:
|
||||
|
||||
@@ -58,6 +58,36 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
|
||||
assert.Contains(t, ctr.Config.Env, env)
|
||||
}
|
||||
|
||||
// Healthcheck can only be compared if set in the spec. Otherwise, the container inherits it from the image.
|
||||
if spec.Container.Healthcheck != nil {
|
||||
hc := spec.Container.Healthcheck
|
||||
require.NotNil(t, ctr.Config.Healthcheck)
|
||||
if hc.Disable {
|
||||
assert.Equal(t, []string{"NONE"}, ctr.Config.Healthcheck.Test)
|
||||
} else {
|
||||
// Only compare fields that are explicitly set in the spec as unset fields inherit their values
|
||||
// from the image.
|
||||
if hc.Test != nil {
|
||||
assert.EqualValues(t, hc.Test, ctr.Config.Healthcheck.Test)
|
||||
}
|
||||
if hc.Interval != 0 {
|
||||
assert.Equal(t, hc.Interval, ctr.Config.Healthcheck.Interval)
|
||||
}
|
||||
if hc.Timeout != 0 {
|
||||
assert.Equal(t, hc.Timeout, ctr.Config.Healthcheck.Timeout)
|
||||
}
|
||||
if hc.StartPeriod != 0 {
|
||||
assert.Equal(t, hc.StartPeriod, ctr.Config.Healthcheck.StartPeriod)
|
||||
}
|
||||
if hc.StartInterval != 0 {
|
||||
assert.Equal(t, hc.StartInterval, ctr.Config.Healthcheck.StartInterval)
|
||||
}
|
||||
if hc.Retries != 0 {
|
||||
assert.Equal(t, int(hc.Retries), ctr.Config.Healthcheck.Retries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, spec.Container.Image, ctr.Config.Image)
|
||||
assert.Equal(t, spec.Container.Init, ctr.HostConfig.Init)
|
||||
assert.True(t, strings.HasPrefix(ctr.Name, spec.Name+"-"))
|
||||
|
||||
@@ -1315,6 +1315,7 @@ func TestServiceLifecycle(t *testing.T) {
|
||||
Name: "container-spec-full",
|
||||
Mode: api.ServiceModeGlobal,
|
||||
Container: api.ContainerSpec{
|
||||
// TODO: Add the latest implemented fields to this spec and update assertContainerMatchesSpec.
|
||||
Command: []string{"sleep", "infinity"},
|
||||
// Extra slashes is not a typo, it changes the spec but Linux ignores them and uses the default /pause.
|
||||
Entrypoint: []string{"///pause"},
|
||||
@@ -1324,6 +1325,14 @@ func TestServiceLifecycle(t *testing.T) {
|
||||
"BOOL": "true",
|
||||
"": "ignored",
|
||||
},
|
||||
Healthcheck: &api.HealthcheckSpec{
|
||||
Test: []string{"CMD-SHELL", "exit 0"},
|
||||
Interval: 1*time.Minute + 30*time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
Retries: 5,
|
||||
StartPeriod: 15 * time.Second,
|
||||
StartInterval: 2 * time.Second,
|
||||
},
|
||||
Image: "portainer/pause:latest",
|
||||
Init: &init,
|
||||
LogDriver: &api.LogDriver{
|
||||
|
||||
@@ -3,67 +3,68 @@
|
||||
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 |
|
||||
| `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` 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` 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) |
|
||||
| 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 | Only `order` supported (defaults to `start-first`). See [deployment strategies](../4-guides/1-deployments/4-deployment-strategies.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 |
|
||||
| **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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user