Skip to content

Commit 9b76bcc

Browse files
authored
Merge pull request #2785 from sazzy4o/main
Support systemd in containers with podman-style `--systemd` flag
2 parents 8f894f8 + 741d06e commit 9b76bcc

File tree

8 files changed

+171
-1
lines changed

8 files changed

+171
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ Minor:
203203
- Better multi-platform support, e.g., `nerdctl pull --all-platforms IMAGE`
204204
- Applying an (existing) AppArmor profile to rootless containers: `nerdctl run --security-opt apparmor=<PROFILE>`.
205205
Use `sudo nerdctl apparmor load` to load the `nerdctl-default` profile.
206+
- Systemd compatibility support: `nerdctl run --systemd=always`
206207

207208
Trivial:
208209

cmd/nerdctl/container_create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
255255
if err != nil {
256256
return
257257
}
258+
opt.Systemd, err = cmd.Flags().GetString("systemd")
259+
if err != nil {
260+
return
261+
}
258262
// #endregion
259263

260264
// #region for runtime flags

cmd/nerdctl/container_run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ func setCreateFlags(cmd *cobra.Command) {
184184
cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities")
185185
cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete)
186186
cmd.Flags().Bool("privileged", false, "Give extended privileges to this container")
187+
cmd.Flags().String("systemd", "false", "Allow running systemd in this container (default: false)")
187188
// #endregion
188189

189190
// #region runtime flags
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"testing"
21+
22+
"github.com/containerd/nerdctl/v2/pkg/testutil"
23+
)
24+
25+
func TestRunWithSystemdAlways(t *testing.T) {
26+
testutil.DockerIncompatible(t)
27+
t.Parallel()
28+
base := testutil.NewBase(t)
29+
containerName := testutil.Identifier(t)
30+
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
31+
32+
base.Cmd("run", "--name", containerName, "--systemd=always", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(rw,")
33+
34+
base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
35+
36+
}
37+
38+
func TestRunWithSystemdTrueEnabled(t *testing.T) {
39+
testutil.DockerIncompatible(t)
40+
t.Parallel()
41+
base := testutil.NewBase(t)
42+
containerName := testutil.Identifier(t)
43+
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
44+
45+
base.Cmd("run", "-d", "--name", containerName, "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK()
46+
47+
base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
48+
49+
base.Cmd("exec", containerName, "systemctl", "list-jobs").AssertOutContains("jobs listed.")
50+
}
51+
52+
func TestRunWithSystemdTrueDisabled(t *testing.T) {
53+
testutil.DockerIncompatible(t)
54+
t.Parallel()
55+
base := testutil.NewBase(t)
56+
containerName := testutil.Identifier(t)
57+
defer base.Cmd("rm", "-f", containerName).AssertOK()
58+
59+
base.Cmd("run", "--name", containerName, "--systemd=true", "--entrypoint=/bin/bash", testutil.SystemdImage, "-c", "systemctl list-jobs || true").AssertCombinedOutContains("System has not been booted with systemd as init system")
60+
}
61+
62+
func TestRunWithSystemdFalse(t *testing.T) {
63+
testutil.DockerIncompatible(t)
64+
t.Parallel()
65+
base := testutil.NewBase(t)
66+
containerName := testutil.Identifier(t)
67+
defer base.Cmd("rm", "-f", containerName).AssertOK()
68+
69+
base.Cmd("run", "--name", containerName, "--systemd=false", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,")
70+
71+
base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM")
72+
}
73+
74+
func TestRunWithNoSystemd(t *testing.T) {
75+
testutil.DockerIncompatible(t)
76+
t.Parallel()
77+
base := testutil.NewBase(t)
78+
containerName := testutil.Identifier(t)
79+
defer base.Cmd("rm", "-f", containerName).AssertOK()
80+
81+
base.Cmd("run", "--name", containerName, "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,")
82+
83+
base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM")
84+
}
85+
86+
func TestRunWithSystemdPrivilegedError(t *testing.T) {
87+
testutil.DockerIncompatible(t)
88+
t.Parallel()
89+
base := testutil.NewBase(t)
90+
91+
base.Cmd("run", "--privileged", "--rm", "--systemd=always", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertCombinedOutContains("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")
92+
}
93+
94+
func TestRunWithSystemdPrivilegedSuccess(t *testing.T) {
95+
testutil.DockerIncompatible(t)
96+
t.Parallel()
97+
base := testutil.NewBase(t)
98+
containerName := testutil.Identifier(t)
99+
defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
100+
101+
base.Cmd("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK()
102+
103+
base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
104+
}

docs/command-reference.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ Security flags:
232232
- :whale: `--cap-add=<CAP>`: Add Linux capabilities
233233
- :whale: `--cap-drop=<CAP>`: Drop Linux capabilities
234234
- :whale: `--privileged`: Give extended privileges to this container
235+
- :nerd_face: `--systemd=(true|false|always)`: Enable systemd compatibility (default: false).
236+
- Default: "false"
237+
- true: Enable systemd compatibility is enabled if the entrypoint executable matches one of the following paths:
238+
- `/sbin/init`
239+
- `/usr/sbin/init`
240+
- `/usr/local/sbin/init`
241+
- always: Always enable systemd compatibility
242+
243+
Corresponds to Podman CLI.
235244

236245
Runtime flags:
237246

pkg/api/types/container_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ type ContainerCreateOptions struct {
169169
CapDrop []string
170170
// Privileged gives extended privileges to this container
171171
Privileged bool
172+
// Systemd
173+
Systemd string
172174
// #endregion
173175

174176
// #region for runtime flags

pkg/cmd/container/create.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"github.com/containerd/nerdctl/v2/pkg/ipcutil"
4747
"github.com/containerd/nerdctl/v2/pkg/labels"
4848
"github.com/containerd/nerdctl/v2/pkg/logging"
49+
"github.com/containerd/nerdctl/v2/pkg/maputil"
4950
"github.com/containerd/nerdctl/v2/pkg/mountutil"
5051
"github.com/containerd/nerdctl/v2/pkg/namestore"
5152
"github.com/containerd/nerdctl/v2/pkg/platformutil"
@@ -173,7 +174,6 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
173174
return nil, nil, err
174175
}
175176
cOpts = append(cOpts, restartOpts...)
176-
cOpts = append(cOpts, withStop(options.StopSignal, options.StopTimeout, ensuredImage))
177177

178178
if err = netManager.VerifyNetworkOptions(ctx); err != nil {
179179
return nil, nil, fmt.Errorf("failed to verify networking settings: %s", err)
@@ -340,6 +340,15 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
340340
opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv)
341341
}
342342

343+
entrypointPath := ""
344+
if ensured != nil {
345+
if len(ensured.ImageConfig.Entrypoint) > 0 {
346+
entrypointPath = ensured.ImageConfig.Entrypoint[0]
347+
} else if len(ensured.ImageConfig.Cmd) > 0 {
348+
entrypointPath = ensured.ImageConfig.Cmd[0]
349+
}
350+
}
351+
343352
if !options.Rootfs && !options.EntrypointChanged {
344353
opts = append(opts, oci.WithImageConfigArgs(ensured.Image, args[1:]))
345354
} else {
@@ -357,8 +366,47 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
357366
// error message is from Podman
358367
return nil, nil, errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image")
359368
}
369+
370+
entrypointPath = processArgs[0]
371+
360372
opts = append(opts, oci.WithProcessArgs(processArgs...))
361373
}
374+
375+
isEntryPointSystemd := (entrypointPath == "/sbin/init" ||
376+
entrypointPath == "/usr/sbin/init" ||
377+
entrypointPath == "/usr/local/sbin/init")
378+
379+
stopSignal := options.StopSignal
380+
381+
if options.Systemd == "always" || (options.Systemd == "true" && isEntryPointSystemd) {
382+
if options.Privileged {
383+
securityOptsMap := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(options.SecurityOpt))
384+
privilegedWithoutHostDevices, err := maputil.MapBoolValueAsOpt(securityOptsMap, "privileged-without-host-devices")
385+
if err != nil {
386+
return nil, nil, err
387+
}
388+
389+
// See: https://github.com/containers/podman/issues/15878
390+
if !privilegedWithoutHostDevices {
391+
return nil, nil, errors.New("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")
392+
}
393+
}
394+
395+
opts = append(opts,
396+
oci.WithoutMounts("/sys/fs/cgroup"),
397+
oci.WithMounts([]specs.Mount{
398+
{Type: "cgroup", Source: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"rw"}},
399+
{Type: "tmpfs", Source: "tmpfs", Destination: "/run"},
400+
{Type: "tmpfs", Source: "tmpfs", Destination: "/run/lock"},
401+
{Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
402+
{Type: "tmpfs", Source: "tmpfs", Destination: "/var/lib/journal"},
403+
}),
404+
)
405+
stopSignal = "SIGRTMIN+3"
406+
}
407+
408+
cOpts = append(cOpts, withStop(stopSignal, options.StopTimeout, ensured))
409+
362410
if options.InitBinary != nil {
363411
options.InitProcessFlag = true
364412
}

pkg/testutil/testutil_linux.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var (
4141
DockerAuthImage = mirrorOf("cesanta/docker_auth:1.7")
4242
FluentdImage = mirrorOf("fluent/fluentd:v1.14-1")
4343
KuboImage = mirrorOf("ipfs/kubo:v0.16.0")
44+
SystemdImage = "ghcr.io/containerd/stargz-snapshotter:0.15.1-kind"
4445

4546
// Source: https://gist.github.com/cpuguy83/fcf3041e5d8fb1bb5c340915aabeebe0
4647
NonDistBlobImage = "ghcr.io/cpuguy83/non-dist-blob:latest"

0 commit comments

Comments
 (0)