mirror of
https://github.com/apple/container.git
synced 2026-08-28 11:26:32 +00:00
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.
This commit is contained in:
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "a0d9f0581740922266b0739fae8ec0998c7d5c7d98ff76cccb68a878f27e88ab",
|
||||
"originHash" : "052cce484c36a6e3389f4630d1fbaf7a73c8902737f282ec0697eedc84c833e9",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
@@ -15,8 +15,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/containerization.git",
|
||||
"state" : {
|
||||
"revision" : "185b04af7414db37577e003bad052cc8fc75a5c9",
|
||||
"version" : "0.26.2"
|
||||
"revision" : "185bc1ecd7b6d9ef1938da1846e620ab73db8950",
|
||||
"version" : "0.26.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import PackageDescription
|
||||
let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
|
||||
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
|
||||
let builderShimVersion = "0.8.0"
|
||||
let scVersion = "0.26.2"
|
||||
let scVersion = "0.26.3"
|
||||
|
||||
let package = Package(
|
||||
name: "container",
|
||||
|
||||
@@ -51,6 +51,8 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
public var ssh: Bool = false
|
||||
/// Whether to mount the rootfs as read-only.
|
||||
public var readOnly: Bool = false
|
||||
/// Whether to use a minimal init process inside the container.
|
||||
public var useInit: Bool = false
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
@@ -70,6 +72,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
case virtualization
|
||||
case ssh
|
||||
case readOnly
|
||||
case useInit
|
||||
}
|
||||
|
||||
/// Create a configuration from the supplied Decoder, initializing missing
|
||||
@@ -100,6 +103,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false
|
||||
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
|
||||
readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||
useInit = try container.decodeIfPresent(Bool.self, forKey: .useInit) ?? false
|
||||
}
|
||||
|
||||
public struct DNSConfiguration: Sendable, Codable {
|
||||
|
||||
@@ -188,6 +188,7 @@ public struct Flags {
|
||||
runtime: String?,
|
||||
ssh: Bool,
|
||||
tmpFs: [String],
|
||||
useInit: Bool,
|
||||
virtualization: Bool,
|
||||
volumes: [String]
|
||||
) {
|
||||
@@ -213,6 +214,7 @@ public struct Flags {
|
||||
self.runtime = runtime
|
||||
self.ssh = ssh
|
||||
self.tmpFs = tmpFs
|
||||
self.useInit = useInit
|
||||
self.virtualization = virtualization
|
||||
self.volumes = volumes
|
||||
}
|
||||
@@ -238,6 +240,15 @@ public struct Flags {
|
||||
)
|
||||
public var entrypoint: String?
|
||||
|
||||
@Flag(name: .customLong("init"), help: "Run an init process inside the container that forwards signals and reaps processes")
|
||||
public var useInit = false
|
||||
|
||||
@Option(
|
||||
name: .long,
|
||||
help: .init("Use a custom init image instead of the default", valueName: "image")
|
||||
)
|
||||
public var initImage: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: .init("Set a custom kernel path", valueName: "path"),
|
||||
@@ -248,12 +259,6 @@ public struct Flags {
|
||||
)
|
||||
public var kernel: String?
|
||||
|
||||
@Option(
|
||||
name: .long,
|
||||
help: .init("Use a custom init image instead of the default", valueName: "image")
|
||||
)
|
||||
public var initImage: String?
|
||||
|
||||
@Option(name: [.short, .customLong("label")], help: "Add a key=value label to the container")
|
||||
public var labels: [String] = []
|
||||
|
||||
@@ -293,21 +298,24 @@ public struct Flags {
|
||||
)
|
||||
public var publishSockets: [String] = []
|
||||
|
||||
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
|
||||
public var readOnly = false
|
||||
|
||||
@Flag(name: [.customLong("rm"), .long], help: "Remove the container after it stops")
|
||||
public var remove = false
|
||||
|
||||
@Flag(name: .long, help: "Enable Rosetta in the container")
|
||||
public var rosetta = false
|
||||
|
||||
@Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)")
|
||||
public var runtime: String?
|
||||
|
||||
@Flag(name: .long, help: "Forward SSH agent socket to container")
|
||||
public var ssh = false
|
||||
|
||||
@Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path")
|
||||
public var tmpFs: [String] = []
|
||||
|
||||
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
|
||||
public var volumes: [String] = []
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help:
|
||||
@@ -315,11 +323,8 @@ public struct Flags {
|
||||
)
|
||||
public var virtualization: Bool = false
|
||||
|
||||
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
|
||||
public var readOnly = false
|
||||
|
||||
@Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)")
|
||||
public var runtime: String?
|
||||
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
|
||||
public var volumes: [String] = []
|
||||
}
|
||||
|
||||
public struct Progress: ParsableArguments {
|
||||
|
||||
@@ -248,6 +248,7 @@ public struct Utility {
|
||||
|
||||
config.ssh = management.ssh
|
||||
config.readOnly = management.readOnly
|
||||
config.useInit = management.useInit
|
||||
|
||||
if let runtime = management.runtime {
|
||||
config.runtimeHandler = runtime
|
||||
|
||||
@@ -842,6 +842,7 @@ public actor SandboxService {
|
||||
}
|
||||
// If the host doesn't support this, we'll throw on container creation.
|
||||
czConfig.virtualization = config.virtualization
|
||||
czConfig.useInit = config.useInit
|
||||
|
||||
for mount in config.mounts {
|
||||
if try mount.isSocket() {
|
||||
|
||||
@@ -487,6 +487,74 @@ class TestCLIRunCommand2: CLITest {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRunCommandInit() throws {
|
||||
do {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name, args: ["--init"])
|
||||
defer {
|
||||
try? doStop(name: name)
|
||||
}
|
||||
let inspectResp = try inspectContainer(name)
|
||||
#expect(inspectResp.configuration.useInit == true, "expected useInit to be true in container configuration")
|
||||
|
||||
// With --init, PID 1 should be the init process, not "sleep".
|
||||
var output = try doExec(name: name, cmd: ["cat", "/proc/1/cmdline"])
|
||||
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
#expect(
|
||||
!output.hasPrefix("sleep"),
|
||||
"expected PID 1 to be init process, not 'sleep', got '\(output)'"
|
||||
)
|
||||
try doStop(name: name)
|
||||
} catch {
|
||||
Issue.record("failed to run container with --init: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRunCommandInitReapsZombies() throws {
|
||||
do {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name, args: ["--init"])
|
||||
defer {
|
||||
try? doStop(name: name)
|
||||
}
|
||||
|
||||
_ = try doExec(
|
||||
name: name,
|
||||
cmd: [
|
||||
"sh", "-c",
|
||||
"sh -c 'sh -c \"exit 0\" &' && sleep 1",
|
||||
])
|
||||
|
||||
let psOutput = try doExec(name: name, cmd: ["sh", "-c", "ps aux | grep -c '\\[sh\\]' || true"])
|
||||
let zombieCount = Int(psOutput.trimmingCharacters(in: .whitespacesAndNewlines)) ?? -1
|
||||
#expect(
|
||||
zombieCount == 0,
|
||||
"expected no zombie processes with --init, found \(zombieCount)"
|
||||
)
|
||||
try doStop(name: name)
|
||||
} catch {
|
||||
Issue.record("failed to verify zombie reaping with --init: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRunCommandWithoutInitDefault() throws {
|
||||
do {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name, args: [])
|
||||
defer {
|
||||
try? doStop(name: name)
|
||||
}
|
||||
let inspectResp = try inspectContainer(name)
|
||||
#expect(inspectResp.configuration.useInit == false, "expected useInit to be false by default")
|
||||
try doStop(name: name)
|
||||
} catch {
|
||||
Issue.record("failed to run container without --init: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestCLIRunCommand3: CLITest {
|
||||
|
||||
@@ -50,6 +50,7 @@ container run [<options>] <image> [<arguments> ...]
|
||||
* `--dns-option <option>`: DNS options
|
||||
* `--dns-search <domain>`: DNS search domains
|
||||
* `--entrypoint <cmd>`: Override the entrypoint of the image
|
||||
* `--init`: Run an init process inside the container that forwards signals and reaps processes
|
||||
* `--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.
|
||||
* `-k, --kernel <path>`: Set a custom kernel path
|
||||
* `-l, --label <label>`: Add a key=value label to the container
|
||||
@@ -61,13 +62,14 @@ container run [<options>] <image> [<arguments> ...]
|
||||
* `-p, --publish <spec>`: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])
|
||||
* `--platform <platform>`: Platform for the image if it's multi-platform. This takes precedence over --os and --arch
|
||||
* `--publish-socket <spec>`: Publish a socket from container to host (format: host_path:container_path)
|
||||
* `--read-only`: Mount the container's root filesystem as read-only
|
||||
* `--rm, --remove`: Remove the container after it stops
|
||||
* `--rosetta`: Enable Rosetta in the container
|
||||
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
|
||||
* `--ssh`: Forward SSH agent socket to container
|
||||
* `--tmpfs <tmpfs>`: Add a tmpfs mount to the container at the given path
|
||||
* `-v, --volume <volume>`: Bind mount a volume into the container
|
||||
* `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support)
|
||||
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
|
||||
|
||||
**Registry Options**
|
||||
|
||||
@@ -104,6 +106,9 @@ container run -e NODE_ENV=production --cpus 2 --memory 1G node:18
|
||||
# run a container with a specific MAC address
|
||||
container run --network default,mac=02:42:ac:11:00:02 ubuntu:latest
|
||||
|
||||
# run a container with an init process to reap zombies and forward signals
|
||||
container run --init ubuntu:latest my-app
|
||||
|
||||
# run a container with a custom init image for boot customization
|
||||
container run --init-image local/custom-init:latest ubuntu:latest
|
||||
```
|
||||
@@ -205,6 +210,7 @@ container create [<options>] <image> [<arguments> ...]
|
||||
* `--dns-option <option>`: DNS options
|
||||
* `--dns-search <domain>`: DNS search domains
|
||||
* `--entrypoint <cmd>`: Override the entrypoint of the image
|
||||
* `--init`: Run an init process inside the container that forwards signals and reaps processes
|
||||
* `--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.
|
||||
* `-k, --kernel <path>`: Set a custom kernel path
|
||||
* `-l, --label <label>`: Add a key=value label to the container
|
||||
@@ -216,13 +222,14 @@ container create [<options>] <image> [<arguments> ...]
|
||||
* `-p, --publish <spec>`: Publish a port from container to host (format: [host-ip:]host-port:container-port[/protocol])
|
||||
* `--platform <platform>`: Platform for the image if it's multi-platform. This takes precedence over --os and --arch
|
||||
* `--publish-socket <spec>`: Publish a socket from container to host (format: host_path:container_path)
|
||||
* `--read-only`: Mount the container's root filesystem as read-only
|
||||
* `--rm, --remove`: Remove the container after it stops
|
||||
* `--rosetta`: Enable Rosetta in the container
|
||||
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
|
||||
* `--ssh`: Forward SSH agent socket to container
|
||||
* `--tmpfs <tmpfs>`: Add a tmpfs mount to the container at the given path
|
||||
* `-v, --volume <volume>`: Bind mount a volume into the container
|
||||
* `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support)
|
||||
* `--runtime`: Set the runtime handler for the container (default: container-runtime-linux)
|
||||
|
||||
**Registry Options**
|
||||
|
||||
|
||||
@@ -497,6 +497,21 @@ container run --name nested-virtualization --virtualization --kernel /path/to/a/
|
||||
[ 0.017893] kvm [1]: Hyp mode initialized successfully
|
||||
```
|
||||
|
||||
## Run a container with a provided init process
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
container run --init ubuntu:latest my-app
|
||||
```
|
||||
|
||||
The init process is also available with `container create`:
|
||||
|
||||
```bash
|
||||
container create --init --name my-container ubuntu:latest my-app
|
||||
container start my-container
|
||||
```
|
||||
|
||||
## Use a custom init image
|
||||
|
||||
The `--init-image` flag allows you to specify a custom init filesystem image for the lightweight VM that runs your container. This enables:
|
||||
|
||||
Reference in New Issue
Block a user