Skip to content

Commit 5d6c750

Browse files
authored
CLI: Add read-only flag to run/create (apple#999)
Closes apple#990 Sets the rootfs for a container to read-only.
1 parent aac2457 commit 5d6c750

File tree

6 files changed

+32
-3
lines changed

6 files changed

+32
-3
lines changed

Sources/ContainerResource/Container/Bundle.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,12 @@ extension Bundle {
122122
path.appendingPathComponent(name)
123123
}
124124

125-
public func setContainerRootFs(cloning fs: Filesystem) throws {
126-
let cloned = try fs.clone(to: self.containerRootfsBlock.absolutePath())
125+
public func setContainerRootFs(cloning fs: Filesystem, readonly: Bool = false) throws {
126+
var mutableFs = fs
127+
if readonly && !mutableFs.options.contains("ro") {
128+
mutableFs.options.append("ro")
129+
}
130+
let cloned = try mutableFs.clone(to: self.containerRootfsBlock.absolutePath())
127131
let fsData = try JSONEncoder().encode(cloned)
128132
try fsData.write(to: self.containerRootfsConfig)
129133
}

Sources/ContainerResource/Container/ContainerConfiguration.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public struct ContainerConfiguration: Sendable, Codable {
4949
public var virtualization: Bool = false
5050
/// Enable SSH agent socket forwarding from host to container.
5151
public var ssh: Bool = false
52+
/// Whether to mount the rootfs as read-only.
53+
public var readOnly: Bool = false
5254

5355
enum CodingKeys: String, CodingKey {
5456
case id
@@ -67,6 +69,7 @@ public struct ContainerConfiguration: Sendable, Codable {
6769
case runtimeHandler
6870
case virtualization
6971
case ssh
72+
case readOnly
7073
}
7174

7275
/// Create a configuration from the supplied Decoder, initializing missing
@@ -96,6 +99,7 @@ public struct ContainerConfiguration: Sendable, Codable {
9699
runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux"
97100
virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false
98101
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
102+
readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
99103
}
100104

101105
public struct DNSConfiguration: Sendable, Codable {

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ public struct Flags {
202202
"Expose virtualization capabilities to the container (requires host and guest support)"
203203
)
204204
public var virtualization: Bool = false
205+
206+
@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
207+
public var readOnly = false
205208
}
206209

207210
public struct Progress: ParsableArguments {

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ public struct Utility {
239239
config.publishedSockets = try Parser.publishSockets(management.publishSockets)
240240

241241
config.ssh = management.ssh
242+
config.readOnly = management.readOnly
242243

243244
return (config, kernel)
244245
}

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public actor ContainersService {
243243
do {
244244
let containerImage = ClientImage(description: configuration.image)
245245
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
246-
try bundle.setContainerRootFs(cloning: imageFs)
246+
try bundle.setContainerRootFs(cloning: imageFs, readonly: configuration.readOnly)
247247
try bundle.write(filename: "options.json", value: options)
248248

249249
let snapshot = ContainerSnapshot(

Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,23 @@ class TestCLIRunCommand: CLITest {
639639
}
640640
}
641641

642+
@Test func testRunCommandReadOnly() throws {
643+
do {
644+
let name = getTestName()
645+
try doLongRun(name: name, args: ["--read-only"])
646+
defer {
647+
try? doStop(name: name)
648+
}
649+
// Attempt to touch a file on the read-only rootfs should fail
650+
#expect(throws: (any Error).self) {
651+
try doExec(name: name, cmd: ["touch", "/testfile"])
652+
}
653+
} catch {
654+
Issue.record("failed to run container \(error)")
655+
return
656+
}
657+
}
658+
642659
func getDefaultDomain() throws -> String? {
643660
let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
644661
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")

0 commit comments

Comments
 (0)