Skip to content

Commit dd60ac3

Browse files
authored
Add LinuxRLimit type (#499)
I was not a huge fan of how rlimits were specified using the OCI type as it just takes in strings for the type which isn't super ergonomic. This adds a new type for rlimits that we can convert to oci type like we already do for capabilities.
1 parent d37f900 commit dd60ac3

4 files changed

Lines changed: 422 additions & 3 deletions

File tree

Sources/Containerization/LinuxProcessConfiguration.swift

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,193 @@
1717
import ContainerizationOCI
1818
import ContainerizationOS
1919

20+
/// A resource limit (rlimit) configuration for a container process.
21+
public struct LinuxRLimit: Sendable, Hashable {
22+
/// The kind of resource limit.
23+
public var kind: Kind
24+
/// The hard limit value.
25+
public var hard: UInt64
26+
/// The soft limit value.
27+
public var soft: UInt64
28+
29+
/// Creates a new resource limit.
30+
///
31+
/// - Parameters:
32+
/// - kind: The kind of resource limit.
33+
/// - hard: The hard limit value.
34+
/// - soft: The soft limit value.
35+
public init(kind: Kind, hard: UInt64, soft: UInt64) {
36+
self.kind = kind
37+
self.hard = hard
38+
self.soft = soft
39+
}
40+
41+
/// Creates a new resource limit with the same value for both hard and soft limits.
42+
///
43+
/// - Parameters:
44+
/// - kind: The kind of resource limit.
45+
/// - limit: The limit value for both hard and soft limits.
46+
public init(kind: Kind, limit: UInt64) {
47+
self.kind = kind
48+
self.hard = limit
49+
self.soft = limit
50+
}
51+
52+
/// Convert to OCI POSIXRlimit format for transport.
53+
public func toOCI() -> POSIXRlimit {
54+
POSIXRlimit(type: self.kind.description, hard: self.hard, soft: self.soft)
55+
}
56+
}
57+
58+
extension LinuxRLimit {
59+
/// The kind of resource limit.
60+
public struct Kind: Sendable, Hashable {
61+
private enum Value: Hashable, Sendable, CaseIterable {
62+
case addressSpace
63+
case coreFileSize
64+
case cpuTime
65+
case dataSize
66+
case fileSize
67+
case locks
68+
case lockedMemory
69+
case messageQueue
70+
case nice
71+
case openFiles
72+
case numberOfProcesses
73+
case residentSetSize
74+
case realtimePriority
75+
case realtimeTimeout
76+
case signalsPending
77+
case stackSize
78+
}
79+
80+
private var value: Value
81+
private init(_ value: Value) {
82+
self.value = value
83+
}
84+
85+
/// Maximum size of the process's virtual memory (address space) in bytes.
86+
public static var addressSpace: Self {
87+
Self(.addressSpace)
88+
}
89+
90+
/// Maximum size of a core file in bytes.
91+
public static var coreFileSize: Self {
92+
Self(.coreFileSize)
93+
}
94+
95+
/// Maximum amount of CPU time the process can consume in seconds.
96+
public static var cpuTime: Self {
97+
Self(.cpuTime)
98+
}
99+
100+
/// Maximum size of the process's data segment in bytes.
101+
public static var dataSize: Self {
102+
Self(.dataSize)
103+
}
104+
105+
/// Maximum size of files the process may create in bytes.
106+
public static var fileSize: Self {
107+
Self(.fileSize)
108+
}
109+
110+
/// Maximum number of file locks.
111+
public static var locks: Self {
112+
Self(.locks)
113+
}
114+
115+
/// Maximum number of bytes of memory that may be locked into RAM.
116+
public static var lockedMemory: Self {
117+
Self(.lockedMemory)
118+
}
119+
120+
/// Maximum number of bytes that can be allocated for POSIX message queues.
121+
public static var messageQueue: Self {
122+
Self(.messageQueue)
123+
}
124+
125+
/// Maximum nice value that can be set.
126+
public static var nice: Self {
127+
Self(.nice)
128+
}
129+
130+
/// Maximum number of open file descriptors.
131+
public static var openFiles: Self {
132+
Self(.openFiles)
133+
}
134+
135+
/// Maximum number of processes that can be created by the user.
136+
public static var numberOfProcesses: Self {
137+
Self(.numberOfProcesses)
138+
}
139+
140+
/// Maximum size of the process's resident set (physical memory) in bytes.
141+
public static var residentSetSize: Self {
142+
Self(.residentSetSize)
143+
}
144+
145+
/// Maximum real-time scheduling priority.
146+
public static var realtimePriority: Self {
147+
Self(.realtimePriority)
148+
}
149+
150+
/// Maximum amount of CPU time for real-time scheduling in microseconds.
151+
public static var realtimeTimeout: Self {
152+
Self(.realtimeTimeout)
153+
}
154+
155+
/// Maximum number of signals that may be queued.
156+
public static var signalsPending: Self {
157+
Self(.signalsPending)
158+
}
159+
160+
/// Maximum size of the process stack in bytes.
161+
public static var stackSize: Self {
162+
Self(.stackSize)
163+
}
164+
}
165+
}
166+
167+
extension LinuxRLimit.Kind: CustomStringConvertible {
168+
/// The OCI string representation of the resource limit kind.
169+
public var description: String {
170+
switch self.value {
171+
case .addressSpace:
172+
"RLIMIT_AS"
173+
case .coreFileSize:
174+
"RLIMIT_CORE"
175+
case .cpuTime:
176+
"RLIMIT_CPU"
177+
case .dataSize:
178+
"RLIMIT_DATA"
179+
case .fileSize:
180+
"RLIMIT_FSIZE"
181+
case .locks:
182+
"RLIMIT_LOCKS"
183+
case .lockedMemory:
184+
"RLIMIT_MEMLOCK"
185+
case .messageQueue:
186+
"RLIMIT_MSGQUEUE"
187+
case .nice:
188+
"RLIMIT_NICE"
189+
case .openFiles:
190+
"RLIMIT_NOFILE"
191+
case .numberOfProcesses:
192+
"RLIMIT_NPROC"
193+
case .residentSetSize:
194+
"RLIMIT_RSS"
195+
case .realtimePriority:
196+
"RLIMIT_RTPRIO"
197+
case .realtimeTimeout:
198+
"RLIMIT_RTTIME"
199+
case .signalsPending:
200+
"RLIMIT_SIGPENDING"
201+
case .stackSize:
202+
"RLIMIT_STACK"
203+
}
204+
}
205+
}
206+
20207
/// User-friendly Linux capabilities configuration
21208
public struct LinuxCapabilities: Sendable {
22209
/// Capabilities that define the maximum set of capabilities a process can have
@@ -140,7 +327,7 @@ public struct LinuxProcessConfiguration: Sendable {
140327
/// The user the container process will run as.
141328
public var user: ContainerizationOCI.User = .init()
142329
/// The rlimits for the container process.
143-
public var rlimits: [POSIXRlimit] = []
330+
public var rlimits: [LinuxRLimit] = []
144331
/// The Linux capabilities for the container process.
145332
public var capabilities: LinuxCapabilities = .allCapabilities
146333
/// Whether to allocate a pseudo terminal for the process. If you'd like interactive
@@ -161,7 +348,7 @@ public struct LinuxProcessConfiguration: Sendable {
161348
environmentVariables: [String] = ["PATH=\(Self.defaultPath)"],
162349
workingDirectory: String = "/",
163350
user: ContainerizationOCI.User = .init(),
164-
rlimits: [POSIXRlimit] = [],
351+
rlimits: [LinuxRLimit] = [],
165352
capabilities: LinuxCapabilities = .allCapabilities,
166353
terminal: Bool = false,
167354
stdin: ReaderStream? = nil,
@@ -208,7 +395,7 @@ public struct LinuxProcessConfiguration: Sendable {
208395
env: self.environmentVariables,
209396
capabilities: self.capabilities.toOCI(),
210397
user: self.user,
211-
rlimits: self.rlimits,
398+
rlimits: self.rlimits.map { $0.toOCI() },
212399
terminal: self.terminal
213400
)
214401
}

Sources/Integration/ContainerTests.swift

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,4 +2317,139 @@ extension IntegrationSuite {
23172317
throw error
23182318
}
23192319
}
2320+
2321+
func testRLimitOpenFiles() async throws {
2322+
let id = "test-rlimit-open-files"
2323+
2324+
let bs = try await bootstrap(id)
2325+
let buffer = BufferWriter()
2326+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
2327+
config.process.arguments = ["sh", "-c", "ulimit -n"]
2328+
config.process.rlimits = [
2329+
LinuxRLimit(kind: .openFiles, hard: 2048, soft: 1024)
2330+
]
2331+
config.process.stdout = buffer
2332+
config.bootLog = bs.bootLog
2333+
}
2334+
2335+
try await container.create()
2336+
try await container.start()
2337+
2338+
let status = try await container.wait()
2339+
try await container.stop()
2340+
2341+
guard status.exitCode == 0 else {
2342+
throw IntegrationError.assert(msg: "process status \(status) != 0")
2343+
}
2344+
2345+
guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
2346+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
2347+
}
2348+
2349+
// ulimit -n returns the soft limit
2350+
guard output == "1024" else {
2351+
throw IntegrationError.assert(msg: "expected soft limit '1024', got '\(output)'")
2352+
}
2353+
}
2354+
2355+
func testRLimitMultiple() async throws {
2356+
let id = "test-rlimit-multiple"
2357+
2358+
let bs = try await bootstrap(id)
2359+
let buffer = BufferWriter()
2360+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
2361+
// Read /proc/self/limits to verify multiple rlimits are set
2362+
config.process.arguments = ["cat", "/proc/self/limits"]
2363+
config.process.rlimits = [
2364+
LinuxRLimit(kind: .openFiles, hard: 4096, soft: 2048),
2365+
LinuxRLimit(kind: .stackSize, hard: 16_777_216, soft: 8_388_608),
2366+
LinuxRLimit(kind: .coreFileSize, hard: 0, soft: 0),
2367+
]
2368+
config.process.stdout = buffer
2369+
config.bootLog = bs.bootLog
2370+
}
2371+
2372+
try await container.create()
2373+
try await container.start()
2374+
2375+
let status = try await container.wait()
2376+
try await container.stop()
2377+
2378+
guard status.exitCode == 0 else {
2379+
throw IntegrationError.assert(msg: "process status \(status) != 0")
2380+
}
2381+
2382+
guard let output = String(data: buffer.data, encoding: .utf8) else {
2383+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
2384+
}
2385+
2386+
// Parse /proc/self/limits and verify the values
2387+
// Format: "Limit Name Soft Limit Hard Limit Units"
2388+
let lines = output.split(separator: "\n")
2389+
2390+
// Helper to find and verify a limit line
2391+
func verifyLimit(name: String, expectedSoft: String, expectedHard: String) throws {
2392+
guard let line = lines.first(where: { $0.contains(name) }) else {
2393+
throw IntegrationError.assert(msg: "limit '\(name)' not found in output")
2394+
}
2395+
let parts = line.split(whereSeparator: { $0.isWhitespace }).map(String.init)
2396+
// The line format varies, but soft and hard are typically the last numeric values before units
2397+
guard parts.contains(expectedSoft) && parts.contains(expectedHard) else {
2398+
throw IntegrationError.assert(
2399+
msg: "limit '\(name)' expected soft=\(expectedSoft) hard=\(expectedHard), got: \(line)")
2400+
}
2401+
}
2402+
2403+
try verifyLimit(name: "Max open files", expectedSoft: "2048", expectedHard: "4096")
2404+
try verifyLimit(name: "Max stack size", expectedSoft: "8388608", expectedHard: "16777216")
2405+
try verifyLimit(name: "Max core file size", expectedSoft: "0", expectedHard: "0")
2406+
}
2407+
2408+
func testRLimitExec() async throws {
2409+
let id = "test-rlimit-exec"
2410+
2411+
let bs = try await bootstrap(id)
2412+
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
2413+
config.process.arguments = ["sleep", "100"]
2414+
config.bootLog = bs.bootLog
2415+
}
2416+
2417+
do {
2418+
try await container.create()
2419+
try await container.start()
2420+
2421+
// Exec a process with rlimits set
2422+
let buffer = BufferWriter()
2423+
let exec = try await container.exec("rlimit-exec") { config in
2424+
config.arguments = ["sh", "-c", "ulimit -n"]
2425+
config.rlimits = [
2426+
LinuxRLimit(kind: .openFiles, hard: 512, soft: 256)
2427+
]
2428+
config.stdout = buffer
2429+
}
2430+
2431+
try await exec.start()
2432+
let status = try await exec.wait()
2433+
try await exec.delete()
2434+
2435+
guard status.exitCode == 0 else {
2436+
throw IntegrationError.assert(msg: "exec status \(status) != 0")
2437+
}
2438+
2439+
guard let output = String(data: buffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
2440+
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
2441+
}
2442+
2443+
guard output == "256" else {
2444+
throw IntegrationError.assert(msg: "expected soft limit '256', got '\(output)'")
2445+
}
2446+
2447+
try await container.kill(SIGKILL)
2448+
try await container.wait()
2449+
try await container.stop()
2450+
} catch {
2451+
try? await container.stop()
2452+
throw error
2453+
}
2454+
}
23202455
}

0 commit comments

Comments
 (0)