diff --git a/Sources/ContainerResource/Container/ContainerConfiguration.swift b/Sources/ContainerResource/Container/ContainerConfiguration.swift index ab9275c5a..5a6bd90e0 100644 --- a/Sources/ContainerResource/Container/ContainerConfiguration.swift +++ b/Sources/ContainerResource/Container/ContainerConfiguration.swift @@ -57,6 +57,8 @@ public struct ContainerConfiguration: Sendable, Codable { public var capAdd: [String] = [] /// Linux capabilities to drop (normalized CAP_* strings, or "ALL"). public var capDrop: [String] = [] + /// Size of /dev/shm in bytes. When nil, the default size is used. + public var shmSize: UInt64? enum CodingKeys: String, CodingKey { case id @@ -79,6 +81,7 @@ public struct ContainerConfiguration: Sendable, Codable { case useInit case capAdd case capDrop + case shmSize } /// Create a configuration from the supplied Decoder, initializing missing @@ -112,6 +115,7 @@ public struct ContainerConfiguration: Sendable, Codable { useInit = try container.decodeIfPresent(Bool.self, forKey: .useInit) ?? false capAdd = try container.decodeIfPresent([String].self, forKey: .capAdd) ?? [] capDrop = try container.decodeIfPresent([String].self, forKey: .capDrop) ?? [] + shmSize = try container.decodeIfPresent(UInt64.self, forKey: .shmSize) } public struct DNSConfiguration: Sendable, Codable { diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index 6a964e8cb..35e4b6bca 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -329,6 +329,9 @@ public struct Flags { @Flag(name: .long, help: "Forward SSH agent socket to container") public var ssh = false + @Option(name: .customLong("shm-size"), help: "Size of /dev/shm (e.g. 64M, 1G)") + public var shmSize: String? + @Option(name: .customLong("tmpfs"), help: "Add a tmpfs mount to the container at the given path") public var tmpFs: [String] = [] diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index e404aa2bc..1c4a37d8e 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -191,6 +191,12 @@ public struct Utility { config.mounts = resolvedMounts + if let shmSizeStr = management.shmSize { + let measurement = try Measurement.parse(parsing: shmSizeStr) + let bytes = measurement.converted(to: .bytes) + config.shmSize = UInt64(bytes.value) + } + config.virtualization = management.virtualization // Parse network specifications with properties diff --git a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift index a13d4e24d..870feae4c 100644 --- a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift @@ -862,6 +862,15 @@ public actor SandboxService { czConfig.virtualization = config.virtualization czConfig.useInit = config.useInit + if let shmSize = config.shmSize { + for i in czConfig.mounts.indices { + if czConfig.mounts[i].destination == "/dev/shm" { + czConfig.mounts[i].options.removeAll { $0.hasPrefix("size=") } + czConfig.mounts[i].options.append("size=\(shmSize)") + } + } + } + for mount in config.mounts { if try mount.isSocket() { let socket = UnixSocketConfiguration( diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift index 67cbc5761..ee5b0271e 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift @@ -389,6 +389,26 @@ class TestCLIRunCommand2: CLITest { } } + @Test func testRunCommandShmSize() throws { + do { + let name = getTestName() + let shmSize = "128m" + let expectedKB = 128 * 1024 + try doLongRun(name: name, args: ["--shm-size", shmSize]) + defer { + try? doStop(name: name) + } + let output = try doExec(name: name, cmd: ["mount"]) + let shmLine = output.split(separator: "\n").first { $0.contains("/dev/shm") } + #expect(shmLine != nil, "expected /dev/shm in mount output") + #expect(shmLine!.contains("size=\(expectedKB)k"), "expected size=\(expectedKB)k in mount options, got: \(shmLine!)") + try doStop(name: name) + } catch { + Issue.record("failed to run container \(error)") + return + } + } + @Test func testRunCommandOSArch() throws { do { let name = getLowercasedTestName()