Skip to content

Commit 899081d

Browse files
authored
cli: Add --init to run/create (#1244)
Closes #1225 Add a flag to signify that we'd like to run a minimal init process that can reap zombie processes. The actual support for this is in the Containerization library so the plumbing here is very simple.
1 parent 9f9a7c9 commit 899081d

9 files changed

Lines changed: 121 additions & 20 deletions

File tree

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import PackageDescription
2323
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
2424
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
2525
let builderShimVersion = "0.8.0"
26-
let scVersion = "0.26.2"
26+
let scVersion = "0.26.3"
2727

2828
let package = Package(
2929
name: "container",

Sources/ContainerResource/Container/ContainerConfiguration.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public struct ContainerConfiguration: Sendable, Codable {
5151
public var ssh: Bool = false
5252
/// Whether to mount the rootfs as read-only.
5353
public var readOnly: Bool = false
54+
/// Whether to use a minimal init process inside the container.
55+
public var useInit: Bool = false
5456

5557
enum CodingKeys: String, CodingKey {
5658
case id
@@ -70,6 +72,7 @@ public struct ContainerConfiguration: Sendable, Codable {
7072
case virtualization
7173
case ssh
7274
case readOnly
75+
case useInit
7376
}
7477

7578
/// Create a configuration from the supplied Decoder, initializing missing
@@ -100,6 +103,7 @@ public struct ContainerConfiguration: Sendable, Codable {
100103
virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false
101104
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
102105
readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
106+
useInit = try container.decodeIfPresent(Bool.self, forKey: .useInit) ?? false
103107
}
104108

105109
public struct DNSConfiguration: Sendable, Codable {

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ public struct Flags {
188188
runtime: String?,
189189
ssh: Bool,
190190
tmpFs: [String],
191+
useInit: Bool,
191192
virtualization: Bool,
192193
volumes: [String]
193194
) {
@@ -213,6 +214,7 @@ public struct Flags {
213214
self.runtime = runtime
214215
self.ssh = ssh
215216
self.tmpFs = tmpFs
217+
self.useInit = useInit
216218
self.virtualization = virtualization
217219
self.volumes = volumes
218220
}
@@ -238,6 +240,15 @@ public struct Flags {
238240
)
239241
public var entrypoint: String?
240242

243+
@Flag(name: .customLong("init"), help: "Run an init process inside the container that forwards signals and reaps processes")
244+
public var useInit = false
245+
246+
@Option(
247+
name: .long,
248+
help: .init("Use a custom init image instead of the default", valueName: "image")
249+
)
250+
public var initImage: String?
251+
241252
@Option(
242253
name: .shortAndLong,
243254
help: .init("Set a custom kernel path", valueName: "path"),
@@ -248,12 +259,6 @@ public struct Flags {
248259
)
249260
public var kernel: String?
250261

251-
@Option(
252-
name: .long,
253-
help: .init("Use a custom init image instead of the default", valueName: "image")
254-
)
255-
public var initImage: String?
256-
257262
@Option(name: [.short, .customLong("label")], help: "Add a key=value label to the container")
258263
public var labels: [String] = []
259264

@@ -293,33 +298,33 @@ public struct Flags {
293298
)
294299
public var publishSockets: [String] = []
295300

301+
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
302+
public var readOnly = false
303+
296304
@Flag(name: [.customLong("rm"), .long], help: "Remove the container after it stops")
297305
public var remove = false
298306

299307
@Flag(name: .long, help: "Enable Rosetta in the container")
300308
public var rosetta = false
301309

310+
@Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)")
311+
public var runtime: String?
312+
302313
@Flag(name: .long, help: "Forward SSH agent socket to container")
303314
public var ssh = false
304315

305316
@Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path")
306317
public var tmpFs: [String] = []
307318

308-
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
309-
public var volumes: [String] = []
310-
311319
@Flag(
312320
name: .long,
313321
help:
314322
"Expose virtualization capabilities to the container (requires host and guest support)"
315323
)
316324
public var virtualization: Bool = false
317325

318-
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
319-
public var readOnly = false
320-
321-
@Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)")
322-
public var runtime: String?
326+
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
327+
public var volumes: [String] = []
323328
}
324329

325330
public struct Progress: ParsableArguments {

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ public struct Utility {
248248

249249
config.ssh = management.ssh
250250
config.readOnly = management.readOnly
251+
config.useInit = management.useInit
251252

252253
if let runtime = management.runtime {
253254
config.runtimeHandler = runtime

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ public actor SandboxService {
842842
}
843843
// If the host doesn't support this, we'll throw on container creation.
844844
czConfig.virtualization = config.virtualization
845+
czConfig.useInit = config.useInit
845846

846847
for mount in config.mounts {
847848
if try mount.isSocket() {

Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,74 @@ class TestCLIRunCommand2: CLITest {
487487
return
488488
}
489489
}
490+
491+
@Test func testRunCommandInit() throws {
492+
do {
493+
let name = getTestName()
494+
try doLongRun(name: name, args: ["--init"])
495+
defer {
496+
try? doStop(name: name)
497+
}
498+
let inspectResp = try inspectContainer(name)
499+
#expect(inspectResp.configuration.useInit == true, "expected useInit to be true in container configuration")
500+
501+
// With --init, PID 1 should be the init process, not "sleep".
502+
var output = try doExec(name: name, cmd: ["cat", "/proc/1/cmdline"])
503+
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
504+
#expect(
505+
!output.hasPrefix("sleep"),
506+
"expected PID 1 to be init process, not 'sleep', got '\(output)'"
507+
)
508+
try doStop(name: name)
509+
} catch {
510+
Issue.record("failed to run container with --init: \(error)")
511+
return
512+
}
513+
}
514+
515+
@Test func testRunCommandInitReapsZombies() throws {
516+
do {
517+
let name = getTestName()
518+
try doLongRun(name: name, args: ["--init"])
519+
defer {
520+
try? doStop(name: name)
521+
}
522+
523+
_ = try doExec(
524+
name: name,
525+
cmd: [
526+
"sh", "-c",
527+
"sh -c 'sh -c \"exit 0\" &' && sleep 1",
528+
])
529+
530+
let psOutput = try doExec(name: name, cmd: ["sh", "-c", "ps aux | grep -c '\\[sh\\]' || true"])
531+
let zombieCount = Int(psOutput.trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
532+
#expect(
533+
zombieCount == 0,
534+
"expected no zombie processes with --init, found \(zombieCount)"
535+
)
536+
try doStop(name: name)
537+
} catch {
538+
Issue.record("failed to verify zombie reaping with --init: \(error)")
539+
return
540+
}
541+
}
542+
543+
@Test func testRunCommandWithoutInitDefault() throws {
544+
do {
545+
let name = getTestName()
546+
try doLongRun(name: name, args: [])
547+
defer {
548+
try? doStop(name: name)
549+
}
550+
let inspectResp = try inspectContainer(name)
551+
#expect(inspectResp.configuration.useInit == false, "expected useInit to be false by default")
552+
try doStop(name: name)
553+
} catch {
554+
Issue.record("failed to run container without --init: \(error)")
555+
return
556+
}
557+
}
490558
}
491559

492560
class TestCLIRunCommand3: CLITest {

docs/command-reference.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ container run [<options>] <image> [<arguments> ...]
5050
* `--dns-option <option>`: DNS options
5151
* `--dns-search <domain>`: DNS search domains
5252
* `--entrypoint <cmd>`: Override the entrypoint of the image
53+
* `--init`: Run an init process inside the container that forwards signals and reaps processes
5354
* `--init-image <image>`: Use a custom init image instead of the default. This allows customizing boot-time behavior before the OCI container starts, such as running VM-level daemons, configuring eBPF filters, or debugging the init process.
5455
* `-k, --kernel <path>`: Set a custom kernel path
5556
* `-l, --label <label>`: Add a key=value label to the container
@@ -61,13 +62,14 @@ container run [<options>] <image> [<arguments> ...]
6162
* `-p, --publish <spec>`: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])
6263
* `--platform <platform>`: Platform for the image if it's multi-platform. This takes precedence over --os and --arch
6364
* `--publish-socket <spec>`: Publish a socket from container to host (format: host_path:container_path)
65+
* `--read-only`: Mount the container's root filesystem as read-only
6466
* `--rm, --remove`: Remove the container after it stops
6567
* `--rosetta`: Enable Rosetta in the container
68+
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
6669
* `--ssh`: Forward SSH agent socket to container
6770
* `--tmpfs <tmpfs>`: Add a tmpfs mount to the container at the given path
6871
* `-v, --volume <volume>`: Bind mount a volume into the container
6972
* `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support)
70-
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
7173

7274
**Registry Options**
7375

@@ -104,6 +106,9 @@ container run -e NODE_ENV=production --cpus 2 --memory 1G node:18
104106
# run a container with a specific MAC address
105107
container run --network default,mac=02:42:ac:11:00:02 ubuntu:latest
106108

109+
# run a container with an init process to reap zombies and forward signals
110+
container run --init ubuntu:latest my-app
111+
107112
# run a container with a custom init image for boot customization
108113
container run --init-image local/custom-init:latest ubuntu:latest
109114
```
@@ -205,6 +210,7 @@ container create [<options>] <image> [<arguments> ...]
205210
* `--dns-option <option>`: DNS options
206211
* `--dns-search <domain>`: DNS search domains
207212
* `--entrypoint <cmd>`: Override the entrypoint of the image
213+
* `--init`: Run an init process inside the container that forwards signals and reaps processes
208214
* `--init-image <image>`: Use a custom init image instead of the default. This allows customizing boot-time behavior before the OCI container starts, such as running VM-level daemons, configuring eBPF filters, or debugging the init process.
209215
* `-k, --kernel <path>`: Set a custom kernel path
210216
* `-l, --label <label>`: Add a key=value label to the container
@@ -216,13 +222,14 @@ container create [<options>] <image> [<arguments> ...]
216222
* `-p, --publish <spec>`: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])
217223
* `--platform <platform>`: Platform for the image if it's multi-platform. This takes precedence over --os and --arch
218224
* `--publish-socket <spec>`: Publish a socket from container to host (format: host_path:container_path)
225+
* `--read-only`: Mount the container's root filesystem as read-only
219226
* `--rm, --remove`: Remove the container after it stops
220227
* `--rosetta`: Enable Rosetta in the container
228+
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
221229
* `--ssh`: Forward SSH agent socket to container
222230
* `--tmpfs <tmpfs>`: Add a tmpfs mount to the container at the given path
223231
* `-v, --volume <volume>`: Bind mount a volume into the container
224232
* `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support)
225-
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
226233

227234
**Registry Options**
228235

docs/how-to.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,21 @@ container run --name nested-virtualization --virtualization --kernel /path/to/a/
497497
[ 0.017893] kvm [1]: Hyp mode initialized successfully
498498
```
499499

500+
## Run a container with a provided init process
501+
502+
By default, the command you specify in `container run` runs as PID 1 inside the container. This means it is responsible for reaping zombie processes and handling signals, which many applications are not designed to do. The `--init` flag runs a lightweight init process as PID 1 that automatically forwards signals and reaps orphaned child processes.
503+
504+
```bash
505+
container run --init ubuntu:latest my-app
506+
```
507+
508+
The init process is also available with `container create`:
509+
510+
```bash
511+
container create --init --name my-container ubuntu:latest my-app
512+
container start my-container
513+
```
514+
500515
## Use a custom init image
501516

502517
The `--init-image` flag allows you to specify a custom init filesystem image for the lightweight VM that runs your container. This enables:

0 commit comments

Comments
 (0)