Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Sources/ContainerResource/Container/Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,12 @@ extension Bundle {
path.appendingPathComponent(name)
}

public func setContainerRootFs(cloning fs: Filesystem) throws {
let cloned = try fs.clone(to: self.containerRootfsBlock.absolutePath())
public func setContainerRootFs(cloning fs: Filesystem, readonly: Bool = false) throws {
var mutableFs = fs
if readonly && !mutableFs.options.contains("ro") {
mutableFs.options.append("ro")
}
let cloned = try mutableFs.clone(to: self.containerRootfsBlock.absolutePath())
let fsData = try JSONEncoder().encode(cloned)
try fsData.write(to: self.containerRootfsConfig)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public struct ContainerConfiguration: Sendable, Codable {
public var virtualization: Bool = false
/// Enable SSH agent socket forwarding from host to container.
public var ssh: Bool = false
/// Whether to mount the rootfs as read-only.
public var readOnly: Bool = false

enum CodingKeys: String, CodingKey {
case id
Expand All @@ -67,6 +69,7 @@ public struct ContainerConfiguration: Sendable, Codable {
case runtimeHandler
case virtualization
case ssh
case readOnly
}

/// Create a configuration from the supplied Decoder, initializing missing
Expand Down Expand Up @@ -96,6 +99,7 @@ public struct ContainerConfiguration: Sendable, Codable {
runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux"
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
}

public struct DNSConfiguration: Sendable, Codable {
Expand Down
3 changes: 3 additions & 0 deletions Sources/Services/ContainerAPIService/Client/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ public struct Flags {
"Expose virtualization capabilities to the container (requires host and guest support)"
)
public var virtualization: Bool = false

@Flag(name: .long, help: "Mount the container's root filesystem as read-only")
public var readOnly = false
}

public struct Progress: ParsableArguments {
Expand Down
1 change: 1 addition & 0 deletions Sources/Services/ContainerAPIService/Client/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ public struct Utility {
config.publishedSockets = try Parser.publishSockets(management.publishSockets)

config.ssh = management.ssh
config.readOnly = management.readOnly

return (config, kernel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public actor ContainersService {
do {
let containerImage = ClientImage(description: configuration.image)
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
try bundle.setContainerRootFs(cloning: imageFs)
try bundle.setContainerRootFs(cloning: imageFs, readonly: configuration.readOnly)
try bundle.write(filename: "options.json", value: options)

let snapshot = ContainerSnapshot(
Expand Down
17 changes: 17 additions & 0 deletions Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,23 @@ class TestCLIRunCommand: CLITest {
}
}

@Test func testRunCommandReadOnly() throws {
do {
let name = getTestName()
try doLongRun(name: name, args: ["--read-only"])
defer {
try? doStop(name: name)
}
// Attempt to touch a file on the read-only rootfs should fail
#expect(throws: (any Error).self) {
try doExec(name: name, cmd: ["touch", "/testfile"])
}
} catch {
Issue.record("failed to run container \(error)")
return
}
}

func getDefaultDomain() throws -> String? {
let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")
Expand Down