Set restart policy for managed containers
- commit
- ecdfc27e2d68b712d5fcac726b66f3b49cee08c2
- author
- brice · · 2026-05-05 06:48 UTC
- parent
- 521b9c0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +96 -15
M internal/adapters/out/docker/runtime.go +3 -0
| @@ -193,6 +193,9 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||
| 193 | 193 | CapAdd: capAdd, | |
| 194 | 194 | ReadonlyRootfs: config.ReadOnlyRootFS, | |
| 195 | 195 | } | |
| 196 | + | if config.RestartPolicy != "" { | |
| 197 | + | hostConfig.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(config.RestartPolicy)} | |
| 198 | + | } | |
| 196 | 199 | ||
| 197 | 200 | // Create network configuration for container | |
| 198 | 201 | var networkConfig *network.NetworkingConfig | |
| @@ -193,6 +193,9 @@ func (r *Runtime) CreateContainer(ctx context.Context, config *domain.ContainerC | |||||
| 193 | CapAdd: capAdd, | 193 | CapAdd: capAdd, | ||
| 194 | ReadonlyRootfs: config.ReadOnlyRootFS, | 194 | ReadonlyRootfs: config.ReadOnlyRootFS, | ||
| 195 | } | 195 | } | ||
| 196 | + | if config.RestartPolicy != "" { | |||
| 197 | + | hostConfig.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(config.RestartPolicy)} | |||
| 198 | + | } | |||
| 196 | 199 | ||||
| 197 | // Create network configuration for container | 200 | // Create network configuration for container | ||
| 198 | var networkConfig *network.NetworkingConfig | 201 | var networkConfig *network.NetworkingConfig | ||
A internal/adapters/out/docker/runtime_create_test.go +62 -0
| @@ -0,0 +1,62 @@ | |||
| 1 | + | package docker | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/json" | |
| 6 | + | "net/http" | |
| 7 | + | "net/http/httptest" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | ||
| 11 | + | "github.com/docker/docker/client" | |
| 12 | + | "github.com/stretchr/testify/assert" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | ||
| 15 | + | "github.com/bnema/gordon/internal/domain" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | func TestRuntime_CreateContainerAppliesRestartPolicy(t *testing.T) { | |
| 19 | + | var createBody map[string]any | |
| 20 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 21 | + | switch { | |
| 22 | + | case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/create": | |
| 23 | + | require.NoError(t, json.NewDecoder(r.Body).Decode(&createBody)) | |
| 24 | + | w.Header().Set("Content-Type", "application/json") | |
| 25 | + | _, _ = w.Write([]byte(`{"Id":"abc123","Warnings":null}`)) | |
| 26 | + | case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json": | |
| 27 | + | w.Header().Set("Content-Type", "application/json") | |
| 28 | + | _, _ = w.Write([]byte(`{ | |
| 29 | + | "Id":"abc123", | |
| 30 | + | "Name":"/gordon-app.example.com", | |
| 31 | + | "Image":"sha256:image", | |
| 32 | + | "Created":"2026-05-05T00:00:00Z", | |
| 33 | + | "Config":{"Image":"nginx:latest","Labels":{}}, | |
| 34 | + | "State":{"Status":"created","ExitCode":0}, | |
| 35 | + | "NetworkSettings":{"Ports":{}} | |
| 36 | + | }`)) | |
| 37 | + | default: | |
| 38 | + | t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) | |
| 39 | + | } | |
| 40 | + | })) | |
| 41 | + | defer server.Close() | |
| 42 | + | ||
| 43 | + | host := strings.TrimPrefix(server.URL, "http://") | |
| 44 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |
| 45 | + | require.NoError(t, err) | |
| 46 | + | ||
| 47 | + | runtime := NewRuntimeWithClient(cli) | |
| 48 | + | created, err := runtime.CreateContainer(context.Background(), &domain.ContainerConfig{ | |
| 49 | + | Image: "nginx:latest", | |
| 50 | + | Name: "gordon-app.example.com", | |
| 51 | + | RestartPolicy: domain.RestartPolicyAlways, | |
| 52 | + | }) | |
| 53 | + | require.NoError(t, err) | |
| 54 | + | assert.Equal(t, "abc123", created.ID) | |
| 55 | + | ||
| 56 | + | require.NotNil(t, createBody) | |
| 57 | + | hostConfig, ok := createBody["HostConfig"].(map[string]any) | |
| 58 | + | require.True(t, ok) | |
| 59 | + | restartPolicy, ok := hostConfig["RestartPolicy"].(map[string]any) | |
| 60 | + | require.True(t, ok) | |
| 61 | + | assert.Equal(t, domain.RestartPolicyAlways, restartPolicy["Name"]) | |
| 62 | + | } | |
| @@ -0,0 +1,62 @@ | |||||
| 1 | + | package docker | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/json" | |||
| 6 | + | "net/http" | |||
| 7 | + | "net/http/httptest" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | ||||
| 11 | + | "github.com/docker/docker/client" | |||
| 12 | + | "github.com/stretchr/testify/assert" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | ||||
| 15 | + | "github.com/bnema/gordon/internal/domain" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | func TestRuntime_CreateContainerAppliesRestartPolicy(t *testing.T) { | |||
| 19 | + | var createBody map[string]any | |||
| 20 | + | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 21 | + | switch { | |||
| 22 | + | case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/create": | |||
| 23 | + | require.NoError(t, json.NewDecoder(r.Body).Decode(&createBody)) | |||
| 24 | + | w.Header().Set("Content-Type", "application/json") | |||
| 25 | + | _, _ = w.Write([]byte(`{"Id":"abc123","Warnings":null}`)) | |||
| 26 | + | case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json": | |||
| 27 | + | w.Header().Set("Content-Type", "application/json") | |||
| 28 | + | _, _ = w.Write([]byte(`{ | |||
| 29 | + | "Id":"abc123", | |||
| 30 | + | "Name":"/gordon-app.example.com", | |||
| 31 | + | "Image":"sha256:image", | |||
| 32 | + | "Created":"2026-05-05T00:00:00Z", | |||
| 33 | + | "Config":{"Image":"nginx:latest","Labels":{}}, | |||
| 34 | + | "State":{"Status":"created","ExitCode":0}, | |||
| 35 | + | "NetworkSettings":{"Ports":{}} | |||
| 36 | + | }`)) | |||
| 37 | + | default: | |||
| 38 | + | t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) | |||
| 39 | + | } | |||
| 40 | + | })) | |||
| 41 | + | defer server.Close() | |||
| 42 | + | ||||
| 43 | + | host := strings.TrimPrefix(server.URL, "http://") | |||
| 44 | + | cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client())) | |||
| 45 | + | require.NoError(t, err) | |||
| 46 | + | ||||
| 47 | + | runtime := NewRuntimeWithClient(cli) | |||
| 48 | + | created, err := runtime.CreateContainer(context.Background(), &domain.ContainerConfig{ | |||
| 49 | + | Image: "nginx:latest", | |||
| 50 | + | Name: "gordon-app.example.com", | |||
| 51 | + | RestartPolicy: domain.RestartPolicyAlways, | |||
| 52 | + | }) | |||
| 53 | + | require.NoError(t, err) | |||
| 54 | + | assert.Equal(t, "abc123", created.ID) | |||
| 55 | + | ||||
| 56 | + | require.NotNil(t, createBody) | |||
| 57 | + | hostConfig, ok := createBody["HostConfig"].(map[string]any) | |||
| 58 | + | require.True(t, ok) | |||
| 59 | + | restartPolicy, ok := hostConfig["RestartPolicy"].(map[string]any) | |||
| 60 | + | require.True(t, ok) | |||
| 61 | + | assert.Equal(t, domain.RestartPolicyAlways, restartPolicy["Name"]) | |||
| 62 | + | } | |||
M internal/domain/container.go +5 -0
| @@ -63,6 +63,7 @@ type ContainerConfig struct { | |||
| 63 | 63 | WorkingDir string | |
| 64 | 64 | Cmd []string | |
| 65 | 65 | AutoRemove bool | |
| 66 | + | RestartPolicy string | |
| 66 | 67 | Volumes map[string]string // map[containerPath]volumeName | |
| 67 | 68 | ReadOnlyVolumes map[string]string // containerPath -> volumeName (mounted read-only) | |
| 68 | 69 | NetworkMode string // Network to join | |
| @@ -88,3 +89,7 @@ const ( | |||
| 88 | 89 | ContainerStatusPaused ContainerStatus = "paused" | |
| 89 | 90 | ContainerStatusUnknown ContainerStatus = "unknown" | |
| 90 | 91 | ) | |
| 92 | + | ||
| 93 | + | const ( | |
| 94 | + | RestartPolicyAlways = "always" | |
| 95 | + | ) | |
| @@ -63,6 +63,7 @@ type ContainerConfig struct { | |||||
| 63 | WorkingDir string | 63 | WorkingDir string | ||
| 64 | Cmd []string | 64 | Cmd []string | ||
| 65 | AutoRemove bool | 65 | AutoRemove bool | ||
| 66 | + | RestartPolicy string | |||
| 66 | Volumes map[string]string // map[containerPath]volumeName | 67 | Volumes map[string]string // map[containerPath]volumeName | ||
| 67 | ReadOnlyVolumes map[string]string // containerPath -> volumeName (mounted read-only) | 68 | ReadOnlyVolumes map[string]string // containerPath -> volumeName (mounted read-only) | ||
| 68 | NetworkMode string // Network to join | 69 | NetworkMode string // Network to join | ||
| @@ -88,3 +89,7 @@ const ( | |||||
| 88 | ContainerStatusPaused ContainerStatus = "paused" | 89 | ContainerStatusPaused ContainerStatus = "paused" | ||
| 89 | ContainerStatusUnknown ContainerStatus = "unknown" | 90 | ContainerStatusUnknown ContainerStatus = "unknown" | ||
| 90 | ) | 91 | ) | ||
| 92 | + | ||||
| 93 | + | const ( | |||
| 94 | + | RestartPolicyAlways = "always" | |||
| 95 | + | ) | |||
M internal/usecase/container/service.go +17 -15
| @@ -275,18 +275,19 @@ func (s *Service) buildContainerConfig(in containerConfigInput) *domain.Containe | |||
| 275 | 275 | } | |
| 276 | 276 | ||
| 277 | 277 | containerConfig := &domain.ContainerConfig{ | |
| 278 | - | Image: in.ImageRef, | |
| 279 | - | Name: containerName, | |
| 280 | - | Ports: ports, | |
| 281 | - | Env: in.EnvVars, | |
| 282 | - | Volumes: in.Volumes, | |
| 283 | - | NetworkMode: in.NetworkName, | |
| 284 | - | Hostname: in.Domain, | |
| 285 | - | Labels: labels, | |
| 286 | - | AutoRemove: false, | |
| 287 | - | MemoryLimit: cfg.DefaultMemoryLimit, | |
| 288 | - | NanoCPUs: cfg.DefaultNanoCPUs, | |
| 289 | - | PidsLimit: cfg.DefaultPidsLimit, | |
| 278 | + | Image: in.ImageRef, | |
| 279 | + | Name: containerName, | |
| 280 | + | Ports: ports, | |
| 281 | + | Env: in.EnvVars, | |
| 282 | + | Volumes: in.Volumes, | |
| 283 | + | NetworkMode: in.NetworkName, | |
| 284 | + | Hostname: in.Domain, | |
| 285 | + | Labels: labels, | |
| 286 | + | AutoRemove: false, | |
| 287 | + | RestartPolicy: domain.RestartPolicyAlways, | |
| 288 | + | MemoryLimit: cfg.DefaultMemoryLimit, | |
| 289 | + | NanoCPUs: cfg.DefaultNanoCPUs, | |
| 290 | + | PidsLimit: cfg.DefaultPidsLimit, | |
| 290 | 291 | } | |
| 291 | 292 | applySecurityProfile(containerConfig, cfg) | |
| 292 | 293 | return containerConfig | |
| @@ -2619,9 +2620,10 @@ func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, servic | |||
| 2619 | 2620 | domain.LabelEnvHash: envHash, | |
| 2620 | 2621 | domain.LabelImage: serviceImage, | |
| 2621 | 2622 | }, | |
| 2622 | - | MemoryLimit: cfg.DefaultMemoryLimit, | |
| 2623 | - | NanoCPUs: cfg.DefaultNanoCPUs, | |
| 2624 | - | PidsLimit: cfg.DefaultPidsLimit, | |
| 2623 | + | MemoryLimit: cfg.DefaultMemoryLimit, | |
| 2624 | + | NanoCPUs: cfg.DefaultNanoCPUs, | |
| 2625 | + | PidsLimit: cfg.DefaultPidsLimit, | |
| 2626 | + | RestartPolicy: domain.RestartPolicyAlways, | |
| 2625 | 2627 | } | |
| 2626 | 2628 | applySecurityProfile(config, cfg) | |
| 2627 | 2629 | ||
| @@ -275,18 +275,19 @@ func (s *Service) buildContainerConfig(in containerConfigInput) *domain.Containe | |||||
| 275 | } | 275 | } | ||
| 276 | 276 | ||||
| 277 | containerConfig := &domain.ContainerConfig{ | 277 | containerConfig := &domain.ContainerConfig{ | ||
| 278 | - | Image: in.ImageRef, | 278 | + | Image: in.ImageRef, |
| 279 | - | Name: containerName, | 279 | + | Name: containerName, |
| 280 | - | Ports: ports, | 280 | + | Ports: ports, |
| 281 | - | Env: in.EnvVars, | 281 | + | Env: in.EnvVars, |
| 282 | - | Volumes: in.Volumes, | 282 | + | Volumes: in.Volumes, |
| 283 | - | NetworkMode: in.NetworkName, | 283 | + | NetworkMode: in.NetworkName, |
| 284 | - | Hostname: in.Domain, | 284 | + | Hostname: in.Domain, |
| 285 | - | Labels: labels, | 285 | + | Labels: labels, |
| 286 | - | AutoRemove: false, | 286 | + | AutoRemove: false, |
| 287 | - | MemoryLimit: cfg.DefaultMemoryLimit, | 287 | + | RestartPolicy: domain.RestartPolicyAlways, |
| 288 | - | NanoCPUs: cfg.DefaultNanoCPUs, | 288 | + | MemoryLimit: cfg.DefaultMemoryLimit, |
| 289 | - | PidsLimit: cfg.DefaultPidsLimit, | 289 | + | NanoCPUs: cfg.DefaultNanoCPUs, |
| 290 | + | PidsLimit: cfg.DefaultPidsLimit, | |||
| 290 | } | 291 | } | ||
| 291 | applySecurityProfile(containerConfig, cfg) | 292 | applySecurityProfile(containerConfig, cfg) | ||
| 292 | return containerConfig | 293 | return containerConfig | ||
| @@ -2619,9 +2620,10 @@ func (s *Service) deployAttachedService(ctx context.Context, ownerDomain, servic | |||||
| 2619 | domain.LabelEnvHash: envHash, | 2620 | domain.LabelEnvHash: envHash, | ||
| 2620 | domain.LabelImage: serviceImage, | 2621 | domain.LabelImage: serviceImage, | ||
| 2621 | }, | 2622 | }, | ||
| 2622 | - | MemoryLimit: cfg.DefaultMemoryLimit, | 2623 | + | MemoryLimit: cfg.DefaultMemoryLimit, |
| 2623 | - | NanoCPUs: cfg.DefaultNanoCPUs, | 2624 | + | NanoCPUs: cfg.DefaultNanoCPUs, |
| 2624 | - | PidsLimit: cfg.DefaultPidsLimit, | 2625 | + | PidsLimit: cfg.DefaultPidsLimit, |
| 2626 | + | RestartPolicy: domain.RestartPolicyAlways, | |||
| 2625 | } | 2627 | } | ||
| 2626 | applySecurityProfile(config, cfg) | 2628 | applySecurityProfile(config, cfg) | ||
| 2627 | 2629 | ||||
M internal/usecase/container/service_test.go +9 -0
| @@ -4181,6 +4181,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) { | |||
| 4181 | 4181 | assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution") | |
| 4182 | 4182 | assert.Equal(t, "postgres", cfg.Hostname) | |
| 4183 | 4183 | assert.Equal(t, networkName, cfg.NetworkMode) | |
| 4184 | + | assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy) | |
| 4184 | 4185 | }). | |
| 4185 | 4186 | Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil) | |
| 4186 | 4187 | ||
| @@ -4317,3 +4318,11 @@ func TestService_BuildContainerConfig_CompatSecurityProfileDefault(t *testing.T) | |||
| 4317 | 4318 | assert.Nil(t, cfg.CapDrop) | |
| 4318 | 4319 | assert.Nil(t, cfg.CapAdd) | |
| 4319 | 4320 | } | |
| 4321 | + | ||
| 4322 | + | func TestService_BuildContainerConfig_SetsRestartPolicyAlways(t *testing.T) { | |
| 4323 | + | svc := NewService(nil, nil, nil, nil, Config{}, nil) | |
| 4324 | + | ||
| 4325 | + | cfg := svc.buildContainerConfig(containerConfigInput{Domain: "app.example.com", Image: "app:latest", ImageRef: "app:latest"}) | |
| 4326 | + | ||
| 4327 | + | assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy) | |
| 4328 | + | } | |
| @@ -4181,6 +4181,7 @@ func TestDeployAttachedService_SetsAliasOnContainerConfig(t *testing.T) { | |||||
| 4181 | assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution") | 4181 | assert.Equal(t, []string{"postgres"}, cfg.Aliases, "attachment container must have network aliases for DNS resolution") | ||
| 4182 | assert.Equal(t, "postgres", cfg.Hostname) | 4182 | assert.Equal(t, "postgres", cfg.Hostname) | ||
| 4183 | assert.Equal(t, networkName, cfg.NetworkMode) | 4183 | assert.Equal(t, networkName, cfg.NetworkMode) | ||
| 4184 | + | assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy) | |||
| 4184 | }). | 4185 | }). | ||
| 4185 | Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil) | 4186 | Return(&domain.Container{ID: "pg-container-1", Name: containerName}, nil) | ||
| 4186 | 4187 | ||||
| @@ -4317,3 +4318,11 @@ func TestService_BuildContainerConfig_CompatSecurityProfileDefault(t *testing.T) | |||||
| 4317 | assert.Nil(t, cfg.CapDrop) | 4318 | assert.Nil(t, cfg.CapDrop) | ||
| 4318 | assert.Nil(t, cfg.CapAdd) | 4319 | assert.Nil(t, cfg.CapAdd) | ||
| 4319 | } | 4320 | } | ||
| 4321 | + | ||||
| 4322 | + | func TestService_BuildContainerConfig_SetsRestartPolicyAlways(t *testing.T) { | |||
| 4323 | + | svc := NewService(nil, nil, nil, nil, Config{}, nil) | |||
| 4324 | + | ||||
| 4325 | + | cfg := svc.buildContainerConfig(containerConfigInput{Domain: "app.example.com", Image: "app:latest", ImageRef: "app:latest"}) | |||
| 4326 | + | ||||
| 4327 | + | assert.Equal(t, domain.RestartPolicyAlways, cfg.RestartPolicy) | |||
| 4328 | + | } | |||