diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index c05bfb79..8f2437e1 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -623,6 +623,7 @@ func (s *Server) CreateServiceContainer( RestartPolicy: container.RestartPolicy{ Name: container.RestartPolicyUnlessStopped, }, + Sysctls: spec.Container.Sysctls, } // Configure the container to use the internal DNS server if it's available. diff --git a/pkg/api/service.go b/pkg/api/service.go index f075f46f..84a772ef 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -247,6 +247,8 @@ type ContainerSpec struct { PullPolicy string // Resource allocation for the container. Resources ContainerResources + // Namespaced kernel parameters to be set in container + Sysctls map[string]string // User overrides the default user of the image used to run the container. Format: user|UID[:group|GID]. User string // VolumeMounts specifies how volumes are mounted into the container filesystem. @@ -312,6 +314,14 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool { func (s *ContainerSpec) Clone() ContainerSpec { spec := *s + if s.CapAdd != nil { + spec.CapAdd = make([]string, len(s.CapAdd)) + copy(spec.CapAdd, s.CapAdd) + } + if s.CapDrop != nil { + spec.CapDrop = make([]string, len(s.CapDrop)) + copy(spec.CapDrop, s.CapDrop) + } if s.Command != nil { spec.Command = make([]string, len(s.Command)) copy(spec.Command, s.Command) @@ -347,15 +357,12 @@ func (s *ContainerSpec) Clone() ContainerSpec { spec.ConfigMounts[i] = cm.Clone() } } - if s.CapAdd != nil { - spec.CapAdd = make([]string, len(s.CapAdd)) - copy(spec.CapAdd, s.CapAdd) + if s.Sysctls != nil { + spec.Sysctls = make(map[string]string, len(s.Sysctls)) + for k, v := range s.Sysctls { + spec.Sysctls[k] = v + } } - if s.CapDrop != nil { - spec.CapDrop = make([]string, len(s.CapDrop)) - copy(spec.CapDrop, s.CapDrop) - } - return spec } diff --git a/pkg/api/service_test.go b/pkg/api/service_test.go index 1f6ade2a..3654780d 100644 --- a/pkg/api/service_test.go +++ b/pkg/api/service_test.go @@ -232,6 +232,9 @@ func TestContainerSpec_Clone(t *testing.T) { Memory: 2345, MemoryReservation: 3456, }, + Sysctls: map[string]string{ + "net.ipv4.ip_forward": "1", + }, User: "1000:1000", Volumes: []string{"/data", "/config"}, VolumeMounts: []VolumeMount{ @@ -259,6 +262,7 @@ func TestContainerSpec_Clone(t *testing.T) { original.VolumeMounts[0].ContainerPath = stringModified original.ConfigMounts[0].ContainerPath = stringModified *original.ConfigMounts[0].Mode = 0o755 // Modify the Mode pointer value + original.Sysctls["net.ipv4.ip_forward"] = stringModified assert.False(t, original.Equals(cloned)) // Assert cloned values are unchanged @@ -285,4 +289,5 @@ func TestContainerSpec_Clone(t *testing.T) { assert.Equal(t, "/etc/config", cloned.ConfigMounts[0].ContainerPath) assert.NotNil(t, cloned.ConfigMounts[0].Mode) assert.Equal(t, os.FileMode(0o644), *cloned.ConfigMounts[0].Mode, "Mode should be deep copied") + assert.Equal(t, "1", cloned.Sysctls["net.ipv4.ip_forward"]) } diff --git a/pkg/client/compose/service.go b/pkg/client/compose/service.go index b6caa815..86f85f35 100644 --- a/pkg/client/compose/service.go +++ b/pkg/client/compose/service.go @@ -53,6 +53,7 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser Privileged: service.Privileged, PullPolicy: pullPolicy, Resources: resourcesFromCompose(service), + Sysctls: service.Sysctls, User: service.User, }, Name: serviceName, diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 9c800d97..e74dfc4e 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -125,6 +125,9 @@ func TestServiceSpecFromCompose(t *testing.T) { Memory: 100 * units.MiB, MemoryReservation: 50 * units.MiB, }, + Sysctls: map[string]string{ + "net.ipv4.ip_forward": "1", + }, User: "nginx:nginx", VolumeMounts: []api.VolumeMount{ { diff --git a/pkg/client/compose/testdata/compose-full-spec.yaml b/pkg/client/compose/testdata/compose-full-spec.yaml index 48424be1..c7fb89d7 100644 --- a/pkg/client/compose/testdata/compose-full-spec.yaml +++ b/pkg/client/compose/testdata/compose-full-spec.yaml @@ -23,6 +23,8 @@ services: privileged: true pull_policy: always scale: 3 + sysctls: + - net.ipv4.ip_forward=1 user: nginx:nginx volumes: - /etc/passwd:/host/etc/passwd:ro diff --git a/pkg/client/deploy/container_test.go b/pkg/client/deploy/container_test.go index aa40acef..355d63bc 100644 --- a/pkg/client/deploy/container_test.go +++ b/pkg/client/deploy/container_test.go @@ -363,6 +363,27 @@ func TestEvalContainerSpecChange_ContainerPrivileged(t *testing.T) { assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) } +func TestEvalContainerSpecChange_ContainerSysctls(t *testing.T) { + t.Parallel() + + currentSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + } + newSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + Sysctls: map[string]string{ + "net.ipv4.ip_forward": "1", + }, + }, + } + + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) +} + func TestEvalContainerSpecChange_PullPolicy(t *testing.T) { t.Parallel() 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 52267530..bf28e8d1 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -35,6 +35,7 @@ The following table shows the support status for main Compose features: | `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** | | |