mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03: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 == "" {
|
if spec.Mode == "" {
|
||||||
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
|
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.
|
// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
+44
-14
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
@@ -74,14 +75,6 @@ type ServiceSpec struct {
|
|||||||
Configs []ConfigSpec
|
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.
|
// CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined.
|
||||||
func (s *ServiceSpec) CaddyConfig() string {
|
func (s *ServiceSpec) CaddyConfig() string {
|
||||||
if s.Caddy == nil {
|
if s.Caddy == nil {
|
||||||
@@ -252,6 +245,9 @@ type ContainerSpec struct {
|
|||||||
Entrypoint []string
|
Entrypoint []string
|
||||||
// Env defines the environment variables to set inside the container.
|
// Env defines the environment variables to set inside the container.
|
||||||
Env EnvVars
|
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
|
Image string
|
||||||
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
// Run a custom init inside the container. If nil, use the daemon's configured settings.
|
||||||
Init *bool
|
Init *bool
|
||||||
@@ -347,6 +343,17 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
spec.Entrypoint = make([]string, len(s.Entrypoint))
|
||||||
copy(spec.Entrypoint, 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 {
|
if s.LogDriver != nil {
|
||||||
logDriver := *s.LogDriver
|
logDriver := *s.LogDriver
|
||||||
if s.LogDriver.Options != nil {
|
if s.LogDriver.Options != nil {
|
||||||
@@ -354,12 +361,6 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
}
|
}
|
||||||
spec.LogDriver = &logDriver
|
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 {
|
if s.Volumes != nil {
|
||||||
spec.Volumes = make([]string, len(s.Volumes))
|
spec.Volumes = make([]string, len(s.Volumes))
|
||||||
copy(spec.Volumes, s.Volumes)
|
copy(spec.Volumes, s.Volumes)
|
||||||
@@ -389,6 +390,7 @@ func (s *ContainerSpec) Clone() ContainerSpec {
|
|||||||
if s.Resources.DeviceReservations != nil {
|
if s.Resources.DeviceReservations != nil {
|
||||||
spec.Resources.DeviceReservations = slices.Clone(s.Resources.DeviceReservations)
|
spec.Resources.DeviceReservations = slices.Clone(s.Resources.DeviceReservations)
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec
|
return spec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,6 +408,26 @@ func (e EnvVars) ToSlice() []string {
|
|||||||
return env
|
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 {
|
type LogDriver struct {
|
||||||
// Name of the logging driver to use.
|
// Name of the logging driver to use.
|
||||||
Name string
|
Name string
|
||||||
@@ -413,6 +435,14 @@ type LogDriver struct {
|
|||||||
Options map[string]string
|
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 {
|
type RunServiceResponse struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -49,6 +50,7 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
Command: service.Command,
|
Command: service.Command,
|
||||||
Entrypoint: service.Entrypoint,
|
Entrypoint: service.Entrypoint,
|
||||||
Env: env,
|
Env: env,
|
||||||
|
Healthcheck: healthcheckFromCompose(service.HealthCheck),
|
||||||
Image: service.Image,
|
Image: service.Image,
|
||||||
Init: service.Init,
|
Init: service.Init,
|
||||||
Privileged: service.Privileged,
|
Privileged: service.Privileged,
|
||||||
@@ -135,6 +137,34 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
return spec, nil
|
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 {
|
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||||
resources := api.ContainerResources{
|
resources := api.ContainerResources{
|
||||||
CPU: int64(service.CPUS * 1e9),
|
CPU: int64(service.CPUS * 1e9),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
composecli "github.com/compose-spec/compose-go/v2/cli"
|
composecli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@@ -109,6 +110,14 @@ func TestServiceSpecFromCompose(t *testing.T) {
|
|||||||
"EMPTY": "",
|
"EMPTY": "",
|
||||||
"VAR": "value",
|
"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",
|
Image: "nginx:latest",
|
||||||
Init: &initTrue,
|
Init: &initTrue,
|
||||||
LogDriver: &api.LogDriver{
|
LogDriver: &api.LogDriver{
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ services:
|
|||||||
BOOL: "true"
|
BOOL: "true"
|
||||||
EMPTY: ""
|
EMPTY: ""
|
||||||
VAR: value
|
VAR: value
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
start_interval: 2s
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
init: true
|
init: true
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@@ -58,6 +58,36 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
|
|||||||
assert.Contains(t, ctr.Config.Env, env)
|
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.Image, ctr.Config.Image)
|
||||||
assert.Equal(t, spec.Container.Init, ctr.HostConfig.Init)
|
assert.Equal(t, spec.Container.Init, ctr.HostConfig.Init)
|
||||||
assert.True(t, strings.HasPrefix(ctr.Name, spec.Name+"-"))
|
assert.True(t, strings.HasPrefix(ctr.Name, spec.Name+"-"))
|
||||||
|
|||||||
@@ -1315,6 +1315,7 @@ func TestServiceLifecycle(t *testing.T) {
|
|||||||
Name: "container-spec-full",
|
Name: "container-spec-full",
|
||||||
Mode: api.ServiceModeGlobal,
|
Mode: api.ServiceModeGlobal,
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
|
// TODO: Add the latest implemented fields to this spec and update assertContainerMatchesSpec.
|
||||||
Command: []string{"sleep", "infinity"},
|
Command: []string{"sleep", "infinity"},
|
||||||
// Extra slashes is not a typo, it changes the spec but Linux ignores them and uses the default /pause.
|
// Extra slashes is not a typo, it changes the spec but Linux ignores them and uses the default /pause.
|
||||||
Entrypoint: []string{"///pause"},
|
Entrypoint: []string{"///pause"},
|
||||||
@@ -1324,6 +1325,14 @@ func TestServiceLifecycle(t *testing.T) {
|
|||||||
"BOOL": "true",
|
"BOOL": "true",
|
||||||
"": "ignored",
|
"": "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",
|
Image: "portainer/pause:latest",
|
||||||
Init: &init,
|
Init: &init,
|
||||||
LogDriver: &api.LogDriver{
|
LogDriver: &api.LogDriver{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Uncloud supports a subset of the [Compose specification](https://compose-spec.io
|
|||||||
The following table shows the support status for main Compose features:
|
The following table shows the support status for main Compose features:
|
||||||
|
|
||||||
| Feature | Support Status | Notes |
|
| Feature | Support Status | Notes |
|
||||||
|--------------------|--------------------|------------------------------------------------------------------------------------------------|
|
|--------------------|--------------------|---------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **Services** | | |
|
| **Services** | | |
|
||||||
| `build` | ✅ Supported | Build context and Dockerfile |
|
| `build` | ✅ Supported | Build context and Dockerfile |
|
||||||
| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) |
|
| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) |
|
||||||
@@ -20,6 +20,7 @@ The following table shows the support status for main Compose features:
|
|||||||
| `env_file` | ✅ Supported | Environment file |
|
| `env_file` | ✅ Supported | Environment file |
|
||||||
| `environment` | ✅ Supported | Environment variables |
|
| `environment` | ✅ Supported | Environment variables |
|
||||||
| `gpus` | ✅ Supported | GPU device access |
|
| `gpus` | ✅ Supported | GPU device access |
|
||||||
|
| `healthcheck` | ✅ Supported | Health check configuration |
|
||||||
| `image` | ✅ Supported | Container image specification |
|
| `image` | ✅ Supported | Container image specification |
|
||||||
| `init` | ✅ Supported | Run init process in container |
|
| `init` | ✅ Supported | Run init process in container |
|
||||||
| `labels` | ❌ Not supported | |
|
| `labels` | ❌ Not supported | |
|
||||||
@@ -30,7 +31,7 @@ The following table shows the support status for main Compose features:
|
|||||||
| `mem_swappiness` | ❌ Not supported | |
|
| `mem_swappiness` | ❌ Not supported | |
|
||||||
| `memswap_limit` | ❌ Not supported | |
|
| `memswap_limit` | ❌ Not supported | |
|
||||||
| `networks` | ❌ Not supported | All containers share cluster network |
|
| `networks` | ❌ Not supported | All containers share cluster network |
|
||||||
| `ports` | ⚠️ Limited | `mode: host` only, use `x-ports` for HTTP/HTTPS |
|
| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](#x-ports) for HTTP/HTTPS |
|
||||||
| `privileged` | ✅ Supported | Run containers in privileged mode |
|
| `privileged` | ✅ Supported | Run containers in privileged mode |
|
||||||
| `pull_policy` | ✅ Supported | `always`, `missing`, `never` |
|
| `pull_policy` | ✅ Supported | `always`, `missing`, `never` |
|
||||||
| `secrets` | ❌ Not supported | Use configs or environment variables |
|
| `secrets` | ❌ Not supported | Use configs or environment variables |
|
||||||
@@ -42,7 +43,7 @@ The following table shows the support status for main Compose features:
|
|||||||
| **Deploy** | | |
|
| **Deploy** | | |
|
||||||
| `labels` | ❌ Not supported | |
|
| `labels` | ❌ Not supported | |
|
||||||
| `mode` | ✅ Supported | Either `global` or `replicated` |
|
| `mode` | ✅ Supported | Either `global` or `replicated` |
|
||||||
| `placement` | ❌ Not supported | Use `x-machines` extension |
|
| `placement` | ❌ Not supported | Use [`x-machines`](#x-machines) extension |
|
||||||
| `replicas` | ✅ Supported | Number of container replicas |
|
| `replicas` | ✅ Supported | Number of container replicas |
|
||||||
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
|
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
|
||||||
| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
|
| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
|
||||||
|
|||||||
Reference in New Issue
Block a user