diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index 65baa6bc..c05bfb79 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -605,6 +605,8 @@ func (s *Server) CreateServiceContainer( } } hostConfig := &container.HostConfig{ + CapAdd: spec.Container.CapAdd, + CapDrop: spec.Container.CapDrop, Binds: spec.Container.Volumes, Init: spec.Container.Init, Mounts: mounts, diff --git a/pkg/api/service.go b/pkg/api/service.go index 409807f1..f075f46f 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -225,6 +225,10 @@ func (s *ServiceSpec) Clone() ServiceSpec { // ContainerSpec defines the desired state of a container in a service. // ATTENTION: after changing this struct, verify if deploy.EvalContainerSpecChange needs to be updated. type ContainerSpec struct { + // Specifies which additional capabilities should be added for the container. + CapAdd []string + // Specifies which capabilities should be dropped from the container. + CapDrop []string // Command overrides the default CMD of the image to be executed when running a container. Command []string // Entrypoint overrides the default ENTRYPOINT of the image. @@ -343,6 +347,14 @@ 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.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 79508333..1f6ade2a 100644 --- a/pkg/api/service_test.go +++ b/pkg/api/service_test.go @@ -209,6 +209,8 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { func TestContainerSpec_Clone(t *testing.T) { mode := os.FileMode(0o644) original := ContainerSpec{ + CapAdd: []string{"NET_ADMIN"}, + CapDrop: []string{"ALL"}, Command: []string{"sh", "-c", "echo hello"}, Entrypoint: []string{"/bin/bash"}, Env: EnvVars{ @@ -247,6 +249,8 @@ func TestContainerSpec_Clone(t *testing.T) { // Verify deep copy by modifying the original stringModified := "modified" + original.CapAdd[0] = stringModified + original.CapDrop[0] = stringModified original.Command[0] = stringModified original.Entrypoint[0] = stringModified original.Env["FOO"] = stringModified @@ -258,6 +262,8 @@ func TestContainerSpec_Clone(t *testing.T) { assert.False(t, original.Equals(cloned)) // Assert cloned values are unchanged + assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0]) + assert.Equal(t, "ALL", cloned.CapDrop[0]) assert.Equal(t, "sh", cloned.Command[0]) assert.Equal(t, "/bin/bash", cloned.Entrypoint[0]) assert.Equal(t, "bar", cloned.Env["FOO"]) diff --git a/pkg/client/compose/service.go b/pkg/client/compose/service.go index f5be0656..b6caa815 100644 --- a/pkg/client/compose/service.go +++ b/pkg/client/compose/service.go @@ -43,6 +43,8 @@ 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, diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index 335031c6..9c800d97 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -100,6 +100,8 @@ func TestServiceSpecFromCompose(t *testing.T) { Name: "test", Mode: api.ServiceModeReplicated, Container: api.ContainerSpec{ + CapAdd: []string{"NET_ADMIN"}, + CapDrop: []string{"ALL"}, Command: []string{"nginx", "updated", "command"}, Entrypoint: []string{"/updated-docker-entrypoint.sh"}, Env: map[string]string{ diff --git a/pkg/client/compose/testdata/compose-full-spec.yaml b/pkg/client/compose/testdata/compose-full-spec.yaml index 3f36f393..48424be1 100644 --- a/pkg/client/compose/testdata/compose-full-spec.yaml +++ b/pkg/client/compose/testdata/compose-full-spec.yaml @@ -1,5 +1,9 @@ services: test: + cap_add: + - NET_ADMIN + cap_drop: + - ALL command: ["nginx", "updated", "command"] cpus: 0.5 entrypoint: ["/updated-docker-entrypoint.sh"] diff --git a/pkg/client/deploy/container_test.go b/pkg/client/deploy/container_test.go index 3283d4e9..aa40acef 100644 --- a/pkg/client/deploy/container_test.go +++ b/pkg/client/deploy/container_test.go @@ -9,6 +9,44 @@ import ( "github.com/stretchr/testify/assert" ) +func TestEvalContainerSpecChange_ContainerCapAdd(t *testing.T) { + t.Parallel() + + currentSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + } + newSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + CapAdd: []string{"NET_ADMIN"}, + }, + } + + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) +} + +func TestEvalContainerSpecChange_ContainerCapDrop(t *testing.T) { + t.Parallel() + + currentSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + } + newSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + CapDrop: []string{"ALL"}, + }, + } + + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec)) + assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec)) +} + func TestEvalContainerSpecChange_ContainerResources(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 dd155e05..06f5a701 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -7,6 +7,8 @@ The following table shows the support status for main Compose features: |--------------------|--------------------|---------------------------------------------------------------------------------------| | **Services** | | | | `build` | ⚠️ Limited | Build context and Dockerfile | +| `cap_add` | ✅ Supported | Additional kernel capabilities | +| `cap_drop` | ✅ Supported | Which capabilities to drop | | `command` | ✅ Supported | Override container command | | `configs` | ✅ Supported | File-based and inline configs | | `cpus` | ✅ Supported | CPU limit |