From b34cf390a6f8837bb4a6060a7a73a60795ea4d6d Mon Sep 17 00:00:00 2001 From: Charlie Le Date: Tue, 16 Jun 2026 14:13:59 -0700 Subject: [PATCH 1/6] Fix duplicate "(default: 3)" in --max-concurrent-downloads help text (#1725) Remove manually specified default value from help string since ArgumentParser already appends it automatically from the property's default value. Signed-off-by: Charlie Le --- Sources/Services/ContainerAPIService/Client/Flags.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index ec7b83b40..d26eddd75 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -389,7 +389,7 @@ public struct Flags { self.maxConcurrentDownloads = maxConcurrentDownloads } - @Option(name: .long, help: "Maximum number of concurrent downloads (default: 3)") + @Option(name: .long, help: "Maximum number of concurrent downloads") public var maxConcurrentDownloads: Int = 3 } } From 9a7a992d66a5abb012cfbf59b7011296601a3230 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 6 May 2026 09:41:38 -0700 Subject: [PATCH 2/6] Add blkio resource flags --- Package.resolved | 3 +- Package.swift | 2 +- .../Container/ContainerConfiguration.swift | 2 + .../ContainerAPIService/Client/Flags.swift | 50 +++++++- .../ContainerAPIService/Client/Parser.swift | 114 ++++++++++++++++++ .../ContainerAPIService/Client/Utility.swift | 6 + .../RuntimeLinux/Server/RuntimeService.swift | 1 + .../ContainerAPIClientTests/ParserTest.swift | 37 ++++++ 8 files changed, 212 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 70a905580..c68b51a18 100644 --- a/Package.resolved +++ b/Package.resolved @@ -13,10 +13,11 @@ { "identity" : "containerization", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/containerization.git", + "location" : "https://github.com/full-chaos/containerization.git", "state" : { "revision" : "9275f365dd555c8f072e7d250d809f5eb7bdd746", "version" : "0.33.4" + } }, { diff --git a/Package.swift b/Package.swift index e9d8579d0..2b1bb69b4 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,7 @@ let package = Package( .library(name: "MachineAPIService", targets: ["MachineAPIService"]), ], dependencies: [ - .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)), + .package(url: "https://github.com/full-chaos/containerization.git", branch: scVersion), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"), diff --git a/Sources/ContainerResource/Container/ContainerConfiguration.swift b/Sources/ContainerResource/Container/ContainerConfiguration.swift index b1f234915..ec31e5e15 100644 --- a/Sources/ContainerResource/Container/ContainerConfiguration.swift +++ b/Sources/ContainerResource/Container/ContainerConfiguration.swift @@ -154,6 +154,8 @@ public struct ContainerConfiguration: Sendable, Codable { public var cpus: Int = 4 /// Memory in bytes allocated. public var memoryInBytes: UInt64 = 1024.mib() + /// Block I/O resource limits. + public var blockIO: LinuxBlockIO? /// Storage quota/size in bytes. public var storage: UInt64? /// Additional CPU cores allocated for VM overhead (guest agent, etc). diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index d26eddd75..f0ff25e3d 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -101,9 +101,24 @@ public struct Flags { public struct Resource: ParsableArguments { public init() {} - public init(cpus: Int64?, memory: String?) { + public init( + cpus: Int64?, + memory: String?, + blkioWeight: UInt16? = nil, + blkioWeightDevice: [String] = [], + deviceReadBps: [String] = [], + deviceWriteBps: [String] = [], + deviceReadIops: [String] = [], + deviceWriteIops: [String] = [] + ) { self.cpus = cpus self.memory = memory + self.blkioWeight = blkioWeight + self.blkioWeightDevice = blkioWeightDevice + self.deviceReadBps = deviceReadBps + self.deviceWriteBps = deviceWriteBps + self.deviceReadIops = deviceReadIops + self.deviceWriteIops = deviceWriteIops } @Option(name: .shortAndLong, help: "Number of CPUs to allocate to the container") @@ -114,6 +129,39 @@ public struct Flags { help: "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix" ) public var memory: String? + + @Option(name: .customLong("blkio-weight"), help: "Block I/O weight, from 10 to 1000") + public var blkioWeight: UInt16? + + @Option( + name: .customLong("blkio-weight-device"), + help: .init("Block I/O weight for a device (format: :)", valueName: "device-weight") + ) + public var blkioWeightDevice: [String] = [] + + @Option( + name: .customLong("device-read-bps"), + help: .init("Throttle read rate from a device in bytes per second (format: :)", valueName: "device-rate") + ) + public var deviceReadBps: [String] = [] + + @Option( + name: .customLong("device-write-bps"), + help: .init("Throttle write rate to a device in bytes per second (format: :)", valueName: "device-rate") + ) + public var deviceWriteBps: [String] = [] + + @Option( + name: .customLong("device-read-iops"), + help: .init("Throttle read rate from a device in IO operations per second (format: :)", valueName: "device-rate") + ) + public var deviceReadIops: [String] = [] + + @Option( + name: .customLong("device-write-iops"), + help: .init("Throttle write rate to a device in IO operations per second (format: :)", valueName: "device-rate") + ) + public var deviceWriteIops: [String] = [] } public struct DNS: ParsableArguments { diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index 2a6708057..a4fc48f73 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -21,6 +21,7 @@ import ContainerizationError import ContainerizationExtras import ContainerizationOCI import ContainerizationOS +import Darwin import Foundation import SystemPackage @@ -97,6 +98,12 @@ public struct Parser { public static func resources( cpus: Int64?, memory: String?, + blkioWeight: UInt16? = nil, + blkioWeightDevice: [String] = [], + deviceReadBps: [String] = [], + deviceWriteBps: [String] = [], + deviceReadIops: [String] = [], + deviceWriteIops: [String] = [], defaultCPUs: Int, defaultMemory: MemorySize, ) throws -> ContainerConfiguration.Resources { @@ -112,9 +119,116 @@ public struct Parser { resource.memoryInBytes = try Parser.memoryStringAsMiB(memory).mib() } + resource.blockIO = try Parser.blockIO( + weight: blkioWeight, + weightDevice: blkioWeightDevice, + deviceReadBps: deviceReadBps, + deviceWriteBps: deviceWriteBps, + deviceReadIops: deviceReadIops, + deviceWriteIops: deviceWriteIops + ) + return resource } + public static func blockIO( + weight: UInt16?, + weightDevice: [String], + deviceReadBps: [String], + deviceWriteBps: [String], + deviceReadIops: [String], + deviceWriteIops: [String] + ) throws -> LinuxBlockIO? { + let hasBlockIO = weight != nil + || !weightDevice.isEmpty + || !deviceReadBps.isEmpty + || !deviceWriteBps.isEmpty + || !deviceReadIops.isEmpty + || !deviceWriteIops.isEmpty + guard hasBlockIO else { return nil } + + if let weight { + try validateBlockIOWeight(weight) + } + + return try LinuxBlockIO( + weight: weight, + leafWeight: nil, + weightDevice: weightDevice.map { + let parsed = try parseBlockIODeviceSpec($0) + let weight = try parseUInt16(parsed.value, name: "--blkio-weight-device weight") + try validateBlockIOWeight(weight) + return LinuxWeightDevice(major: parsed.device.major, minor: parsed.device.minor, weight: weight, leafWeight: nil) + }, + throttleReadBpsDevice: deviceReadBps.map { + let parsed = try parseBlockIODeviceSpec($0) + return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value)) + }, + throttleWriteBpsDevice: deviceWriteBps.map { + let parsed = try parseBlockIODeviceSpec($0) + return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value)) + }, + throttleReadIOPSDevice: deviceReadIops.map { + let parsed = try parseBlockIODeviceSpec($0) + return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-read-iops rate")) + }, + throttleWriteIOPSDevice: deviceWriteIops.map { + let parsed = try parseBlockIODeviceSpec($0) + return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-write-iops rate")) + } + ) + } + + private static func parseBlockIODeviceSpec(_ value: String) throws -> (device: LinuxBlockIODevice, value: String) { + let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + guard parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else { + throw ContainerizationError(.invalidArgument, message: "block I/O device spec must be ':'") + } + + return try (blockIODevice(path: String(parts[0])), String(parts[1])) + } + + private static func blockIODevice(path: String) throws -> LinuxBlockIODevice { + var info = stat() + guard stat(path, &info) == 0 else { + throw ContainerizationError(.notFound, message: "block I/O device path not found: \(path)") + } + + let rawDevice = UInt32(bitPattern: info.st_rdev) + let major = Int64((rawDevice >> 24) & 0xff) + let minor = Int64(rawDevice & 0x00ff_ffff) + return LinuxBlockIODevice(major: major, minor: minor) + } + + private static func parseByteRate(_ value: String) throws -> UInt64 { + let measurement = try Measurement.parse(parsing: value) + let bytes = measurement.converted(to: .bytes).value + guard bytes >= 0, bytes.rounded(.down) == bytes else { + throw ContainerizationError(.invalidArgument, message: "block I/O byte rate must be a non-negative whole number of bytes") + } + return UInt64(bytes) + } + + private static func parseUInt16(_ value: String, name: String) throws -> UInt16 { + guard let parsed = UInt16(value) else { + throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 16-bit integer") + } + return parsed + } + + private static func parseUInt64(_ value: String, name: String) throws -> UInt64 { + guard let parsed = UInt64(value) else { + throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 64-bit integer") + } + return parsed + } + + private static func validateBlockIOWeight(_ value: UInt16) throws { + guard (10...1000).contains(value) else { + throw ContainerizationError(.invalidArgument, message: "block I/O weight must be between 10 and 1000") + } + } + public static func allEnv(imageEnvs: [String], envFiles: [String], envs: [String]) throws -> [String] { var combined: [String] = [] combined.append(contentsOf: Parser.env(envList: imageEnvs)) diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index bfea2bbc0..ca8cda48a 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -159,6 +159,12 @@ public struct Utility { config.resources = try Parser.resources( cpus: resource.cpus, memory: resource.memory, + blkioWeight: resource.blkioWeight, + blkioWeightDevice: resource.blkioWeightDevice, + deviceReadBps: resource.deviceReadBps, + deviceWriteBps: resource.deviceWriteBps, + deviceReadIops: resource.deviceReadIops, + deviceWriteIops: resource.deviceWriteIops, defaultCPUs: containerSystemConfig.container.cpus, defaultMemory: containerSystemConfig.container.memory ) diff --git a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift index e51ad14b0..7b853493d 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -986,6 +986,7 @@ public actor RuntimeService { czConfig.cpus = config.resources.cpus czConfig.cpuOverhead = config.resources.cpuOverhead czConfig.memoryInBytes = config.resources.memoryInBytes + czConfig.blockIO = config.resources.blockIO czConfig.sysctl = config.sysctls.reduce(into: [String: String]()) { $0[$1.key] = $1.value } diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 8a1deabbe..dbb22c620 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1201,6 +1201,43 @@ struct ParserTest { #expect(result.memoryInBytes == 256.mib()) } + @Test func testResourcesBlockIOFlags() throws { + let result = try Parser.resources( + cpus: nil, + memory: nil, + blkioWeight: 500, + blkioWeightDevice: ["/dev/null:700"], + deviceReadBps: ["/dev/null:1mb"], + deviceWriteBps: ["/dev/null:2mb"], + deviceReadIops: ["/dev/null:1000"], + deviceWriteIops: ["/dev/null:2000"], + defaultCPUs: 8, + defaultMemory: MemorySize("2g") + ) + + let blockIO = try #require(result.blockIO) + #expect(blockIO.weight == 500) + #expect(blockIO.weightDevice.first?.weight == 700) + #expect(blockIO.throttleReadBpsDevice.first?.rate == 1.mib()) + #expect(blockIO.throttleWriteBpsDevice.first?.rate == 2.mib()) + #expect(blockIO.throttleReadIOPSDevice.first?.rate == 1000) + #expect(blockIO.throttleWriteIOPSDevice.first?.rate == 2000) + } + + @Test func testResourcesRejectsInvalidBlockIOWeight() throws { + #expect { + _ = try Parser.resources( + cpus: nil, + memory: nil, + blkioWeight: 1, + defaultCPUs: 8, + defaultMemory: MemorySize("2g") + ) + } throws: { _ in + true + } + } + @Test func testResourcesBuildPropertyLookup() async throws { let content = """ [build] From 739ebe78e0a36009b39e14673f23c474573adc8e Mon Sep 17 00:00:00 2001 From: Chris George Date: Sat, 23 May 2026 17:23:02 -0700 Subject: [PATCH 3/6] Update --blkio CLI shape and adopt containerization PR #739 wrapper Two changes addressing review feedback from https://github.com/apple/containerization/pull/739 and https://github.com/apple/container/issues/1512#issuecomment-4526251917: 1. Adopt the new `Containerization.LinuxBlockIO` wrapper added in containerization PR #739 (pin advanced to 3d009df). The wire format in `ContainerConfiguration.Resources.blockIO` stays as the Codable `ContainerizationOCI.LinuxBlockIO`; `RuntimeService.configureContainer` converts to the wrapper at the boundary via the new `toContainerizationBlockIO` helper. 2. Replace the six separate `--blkio-*` / `--device-*` flags with a single repeatable `--blkio` flag using key=value[,key=value] syntax, per #1512 (comment): --blkio weight=500 --blkio device=/dev/sda,weight=700,leaf-weight=300 --blkio device=/dev/sda,read-bps=1048576,write-bps=1048576 --blkio device=/dev/sda,read-iops=1000,write-iops=1000 Device values accept either an absolute host path (resolved via stat(2)) or a literal `:`. Parser rejects unknown keys, conflicting global weights, and global-only keys appearing on device-less specs. Tests cover the combined spec, major:minor literal, invalid-weight, unknown-key, and global-only-on-device-spec error paths. --- .../ContainerAPIService/Client/Flags.swift | 59 ++--- .../ContainerAPIService/Client/Parser.swift | 227 ++++++++++++------ .../ContainerAPIService/Client/Utility.swift | 7 +- .../RuntimeLinux/Server/RuntimeService.swift | 27 ++- .../ContainerAPIClientTests/ParserTest.swift | 62 ++++- 5 files changed, 254 insertions(+), 128 deletions(-) diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index f0ff25e3d..e78603206 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -104,21 +104,11 @@ public struct Flags { public init( cpus: Int64?, memory: String?, - blkioWeight: UInt16? = nil, - blkioWeightDevice: [String] = [], - deviceReadBps: [String] = [], - deviceWriteBps: [String] = [], - deviceReadIops: [String] = [], - deviceWriteIops: [String] = [] + blkio: [String] = [] ) { self.cpus = cpus self.memory = memory - self.blkioWeight = blkioWeight - self.blkioWeightDevice = blkioWeightDevice - self.deviceReadBps = deviceReadBps - self.deviceWriteBps = deviceWriteBps - self.deviceReadIops = deviceReadIops - self.deviceWriteIops = deviceWriteIops + self.blkio = blkio } @Option(name: .shortAndLong, help: "Number of CPUs to allocate to the container") @@ -130,38 +120,23 @@ public struct Flags { ) public var memory: String? - @Option(name: .customLong("blkio-weight"), help: "Block I/O weight, from 10 to 1000") - public var blkioWeight: UInt16? - - @Option( - name: .customLong("blkio-weight-device"), - help: .init("Block I/O weight for a device (format: :)", valueName: "device-weight") - ) - public var blkioWeightDevice: [String] = [] - - @Option( - name: .customLong("device-read-bps"), - help: .init("Throttle read rate from a device in bytes per second (format: :)", valueName: "device-rate") - ) - public var deviceReadBps: [String] = [] - - @Option( - name: .customLong("device-write-bps"), - help: .init("Throttle write rate to a device in bytes per second (format: :)", valueName: "device-rate") - ) - public var deviceWriteBps: [String] = [] - - @Option( - name: .customLong("device-read-iops"), - help: .init("Throttle read rate from a device in IO operations per second (format: :)", valueName: "device-rate") - ) - public var deviceReadIops: [String] = [] - @Option( - name: .customLong("device-write-iops"), - help: .init("Throttle write rate to a device in IO operations per second (format: :)", valueName: "device-rate") + name: .customLong("blkio"), + help: .init( + """ + Block I/O cgroup tuning (repeatable). Comma-separated key=value pairs: + weight=<10..1000> cgroup-wide weight + leaf-weight=<10..1000> cgroup-wide leaf weight + device=,weight=... per-device weight + device=<...>,read-bps= per-device read throughput limit + device=<...>,write-bps= per-device write throughput limit + device=<...>,read-iops= per-device read IOPS limit + device=<...>,write-iops= per-device write IOPS limit + """, + valueName: "spec" + ) ) - public var deviceWriteIops: [String] = [] + public var blkio: [String] = [] } public struct DNS: ParsableArguments { diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index a4fc48f73..ec922974a 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -98,12 +98,7 @@ public struct Parser { public static func resources( cpus: Int64?, memory: String?, - blkioWeight: UInt16? = nil, - blkioWeightDevice: [String] = [], - deviceReadBps: [String] = [], - deviceWriteBps: [String] = [], - deviceReadIops: [String] = [], - deviceWriteIops: [String] = [], + blkio: [String] = [], defaultCPUs: Int, defaultMemory: MemorySize, ) throws -> ContainerConfiguration.Resources { @@ -119,85 +114,173 @@ public struct Parser { resource.memoryInBytes = try Parser.memoryStringAsMiB(memory).mib() } - resource.blockIO = try Parser.blockIO( - weight: blkioWeight, - weightDevice: blkioWeightDevice, - deviceReadBps: deviceReadBps, - deviceWriteBps: deviceWriteBps, - deviceReadIops: deviceReadIops, - deviceWriteIops: deviceWriteIops - ) + resource.blockIO = try Parser.blockIO(specs: blkio) return resource } - public static func blockIO( - weight: UInt16?, - weightDevice: [String], - deviceReadBps: [String], - deviceWriteBps: [String], - deviceReadIops: [String], - deviceWriteIops: [String] - ) throws -> LinuxBlockIO? { - let hasBlockIO = weight != nil - || !weightDevice.isEmpty - || !deviceReadBps.isEmpty - || !deviceWriteBps.isEmpty - || !deviceReadIops.isEmpty - || !deviceWriteIops.isEmpty - guard hasBlockIO else { return nil } - - if let weight { - try validateBlockIOWeight(weight) + /// Parse `--blkio` specifications into an OCI `LinuxBlockIO`. + /// + /// Each spec is a comma-separated list of `key=value` pairs: + /// + /// --blkio weight=500 + /// --blkio device=/dev/sda,weight=700,leaf-weight=300 + /// --blkio device=/dev/sda,read-bps=1048576,write-bps=1048576 + /// --blkio device=/dev/sda,read-iops=1000,write-iops=1000 + /// + /// Specs without `device=` set cgroup-wide values. Specs with `device=` + /// produce per-device entries; the device value may be a host path + /// (resolved via `stat(2)`) or a `:` literal. + public static func blockIO(specs: [String]) throws -> ContainerizationOCI.LinuxBlockIO? { + guard !specs.isEmpty else { return nil } + + var weight: UInt16? = nil + var leafWeight: UInt16? = nil + var weightDevices: [ContainerizationOCI.LinuxWeightDevice] = [] + var readBpsDevices: [ContainerizationOCI.LinuxThrottleDevice] = [] + var writeBpsDevices: [ContainerizationOCI.LinuxThrottleDevice] = [] + var readIOPSDevices: [ContainerizationOCI.LinuxThrottleDevice] = [] + var writeIOPSDevices: [ContainerizationOCI.LinuxThrottleDevice] = [] + + for spec in specs { + let pairs = try parseBlockIOSpec(spec) + + if let devicePath = pairs["device"] { + let (major, minor) = try parseBlockIODevice(devicePath) + + var deviceWeight: UInt16? = nil + var deviceLeafWeight: UInt16? = nil + + if let raw = pairs["weight"] { + let value = try parseUInt16(raw, name: "weight") + try validateBlockIOWeight(value) + deviceWeight = value + } + if let raw = pairs["leaf-weight"] { + let value = try parseUInt16(raw, name: "leaf-weight") + try validateBlockIOWeight(value) + deviceLeafWeight = value + } + + if deviceWeight != nil || deviceLeafWeight != nil { + weightDevices.append( + ContainerizationOCI.LinuxWeightDevice( + major: major, minor: minor, + weight: deviceWeight, leafWeight: deviceLeafWeight + ) + ) + } + + if let raw = pairs["read-bps"] { + readBpsDevices.append( + ContainerizationOCI.LinuxThrottleDevice(major: major, minor: minor, rate: try parseByteRate(raw)) + ) + } + if let raw = pairs["write-bps"] { + writeBpsDevices.append( + ContainerizationOCI.LinuxThrottleDevice(major: major, minor: minor, rate: try parseByteRate(raw)) + ) + } + if let raw = pairs["read-iops"] { + readIOPSDevices.append( + ContainerizationOCI.LinuxThrottleDevice(major: major, minor: minor, rate: try parseUInt64(raw, name: "read-iops")) + ) + } + if let raw = pairs["write-iops"] { + writeIOPSDevices.append( + ContainerizationOCI.LinuxThrottleDevice(major: major, minor: minor, rate: try parseUInt64(raw, name: "write-iops")) + ) + } + + let allowedDeviceKeys: Set = ["device", "weight", "leaf-weight", "read-bps", "write-bps", "read-iops", "write-iops"] + if let unknown = pairs.keys.first(where: { !allowedDeviceKeys.contains($0) }) { + throw ContainerizationError(.invalidArgument, message: "unknown --blkio key '\(unknown)'") + } + } else { + // Cgroup-wide spec. + if let raw = pairs["weight"] { + let value = try parseUInt16(raw, name: "weight") + try validateBlockIOWeight(value) + if let existing = weight, existing != value { + throw ContainerizationError(.invalidArgument, message: "--blkio weight specified multiple times with conflicting values") + } + weight = value + } + if let raw = pairs["leaf-weight"] { + let value = try parseUInt16(raw, name: "leaf-weight") + try validateBlockIOWeight(value) + if let existing = leafWeight, existing != value { + throw ContainerizationError(.invalidArgument, message: "--blkio leaf-weight specified multiple times with conflicting values") + } + leafWeight = value + } + + let allowedGlobalKeys: Set = ["weight", "leaf-weight"] + if let unknown = pairs.keys.first(where: { !allowedGlobalKeys.contains($0) }) { + throw ContainerizationError( + .invalidArgument, + message: "--blkio key '\(unknown)' is only valid when 'device=' is also set" + ) + } + } } - return try LinuxBlockIO( + return ContainerizationOCI.LinuxBlockIO( weight: weight, - leafWeight: nil, - weightDevice: weightDevice.map { - let parsed = try parseBlockIODeviceSpec($0) - let weight = try parseUInt16(parsed.value, name: "--blkio-weight-device weight") - try validateBlockIOWeight(weight) - return LinuxWeightDevice(major: parsed.device.major, minor: parsed.device.minor, weight: weight, leafWeight: nil) - }, - throttleReadBpsDevice: deviceReadBps.map { - let parsed = try parseBlockIODeviceSpec($0) - return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value)) - }, - throttleWriteBpsDevice: deviceWriteBps.map { - let parsed = try parseBlockIODeviceSpec($0) - return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseByteRate(parsed.value)) - }, - throttleReadIOPSDevice: deviceReadIops.map { - let parsed = try parseBlockIODeviceSpec($0) - return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-read-iops rate")) - }, - throttleWriteIOPSDevice: deviceWriteIops.map { - let parsed = try parseBlockIODeviceSpec($0) - return LinuxThrottleDevice(major: parsed.device.major, minor: parsed.device.minor, rate: try parseUInt64(parsed.value, name: "--device-write-iops rate")) - } + leafWeight: leafWeight, + weightDevice: weightDevices, + throttleReadBpsDevice: readBpsDevices, + throttleWriteBpsDevice: writeBpsDevices, + throttleReadIOPSDevice: readIOPSDevices, + throttleWriteIOPSDevice: writeIOPSDevices ) } - private static func parseBlockIODeviceSpec(_ value: String) throws -> (device: LinuxBlockIODevice, value: String) { - let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) - guard parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else { - throw ContainerizationError(.invalidArgument, message: "block I/O device spec must be ':'") + /// Tokenise a single `--blkio` spec into `key=value` pairs. + private static func parseBlockIOSpec(_ spec: String) throws -> [String: String] { + var result: [String: String] = [:] + for token in spec.split(separator: ",", omittingEmptySubsequences: true) { + let parts = token.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + guard parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else { + throw ContainerizationError( + .invalidArgument, + message: "--blkio entries must use 'key=value' (got '\(token)')" + ) + } + let key = String(parts[0]) + if result[key] != nil { + throw ContainerizationError(.invalidArgument, message: "--blkio key '\(key)' specified twice in a single spec") + } + result[key] = String(parts[1]) } - - return try (blockIODevice(path: String(parts[0])), String(parts[1])) + if result.isEmpty { + throw ContainerizationError(.invalidArgument, message: "--blkio spec must not be empty") + } + return result } - private static func blockIODevice(path: String) throws -> LinuxBlockIODevice { - var info = stat() - guard stat(path, &info) == 0 else { - throw ContainerizationError(.notFound, message: "block I/O device path not found: \(path)") + /// Resolve a `device=` value to (major, minor). Accepts an absolute path + /// (`stat`-ed on the host) or a literal `:`. + private static func parseBlockIODevice(_ value: String) throws -> (major: Int64, minor: Int64) { + if value.hasPrefix("/") { + var info = stat() + guard stat(value, &info) == 0 else { + throw ContainerizationError(.notFound, message: "block I/O device path not found: \(value)") + } + let rawDevice = UInt32(bitPattern: info.st_rdev) + let major = Int64((rawDevice >> 24) & 0xff) + let minor = Int64(rawDevice & 0x00ff_ffff) + return (major, minor) } - let rawDevice = UInt32(bitPattern: info.st_rdev) - let major = Int64((rawDevice >> 24) & 0xff) - let minor = Int64(rawDevice & 0x00ff_ffff) - return LinuxBlockIODevice(major: major, minor: minor) + let parts = value.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + guard parts.count == 2, let major = Int64(parts[0]), let minor = Int64(parts[1]) else { + throw ContainerizationError( + .invalidArgument, + message: "--blkio device must be an absolute path or ':' (got '\(value)')" + ) + } + return (major, minor) } private static func parseByteRate(_ value: String) throws -> UInt64 { @@ -211,14 +294,14 @@ public struct Parser { private static func parseUInt16(_ value: String, name: String) throws -> UInt16 { guard let parsed = UInt16(value) else { - throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 16-bit integer") + throw ContainerizationError(.invalidArgument, message: "--blkio \(name) must be an unsigned 16-bit integer") } return parsed } private static func parseUInt64(_ value: String, name: String) throws -> UInt64 { guard let parsed = UInt64(value) else { - throw ContainerizationError(.invalidArgument, message: "\(name) must be an unsigned 64-bit integer") + throw ContainerizationError(.invalidArgument, message: "--blkio \(name) must be an unsigned 64-bit integer") } return parsed } diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index ca8cda48a..559fc5761 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -159,12 +159,7 @@ public struct Utility { config.resources = try Parser.resources( cpus: resource.cpus, memory: resource.memory, - blkioWeight: resource.blkioWeight, - blkioWeightDevice: resource.blkioWeightDevice, - deviceReadBps: resource.deviceReadBps, - deviceWriteBps: resource.deviceWriteBps, - deviceReadIops: resource.deviceReadIops, - deviceWriteIops: resource.deviceWriteIops, + blkio: resource.blkio, defaultCPUs: containerSystemConfig.container.cpus, defaultMemory: containerSystemConfig.container.memory ) diff --git a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift index 7b853493d..ee8a5e1a4 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -986,7 +986,7 @@ public actor RuntimeService { czConfig.cpus = config.resources.cpus czConfig.cpuOverhead = config.resources.cpuOverhead czConfig.memoryInBytes = config.resources.memoryInBytes - czConfig.blockIO = config.resources.blockIO + czConfig.blockIO = config.resources.blockIO.map(Self.toContainerizationBlockIO) czConfig.sysctl = config.sysctls.reduce(into: [String: String]()) { $0[$1.key] = $1.value } @@ -1193,6 +1193,31 @@ public actor RuntimeService { return Containerization.LinuxCapabilities(capabilities: Array(caps)) } + /// Convert the OCI block I/O wire format (carried in `ContainerConfiguration`) + /// into the `Containerization.LinuxBlockIO` wrapper expected by + /// `LinuxContainer.Configuration`. + private static func toContainerizationBlockIO(_ oci: ContainerizationOCI.LinuxBlockIO) -> Containerization.LinuxBlockIO { + Containerization.LinuxBlockIO( + weight: oci.weight, + leafWeight: oci.leafWeight, + weightDevice: oci.weightDevice.map { + Containerization.LinuxWeightDevice(major: $0.major, minor: $0.minor, weight: $0.weight, leafWeight: $0.leafWeight) + }, + throttleReadBpsDevice: oci.throttleReadBpsDevice.map { + Containerization.LinuxThrottleDevice(major: $0.major, minor: $0.minor, rate: $0.rate) + }, + throttleWriteBpsDevice: oci.throttleWriteBpsDevice.map { + Containerization.LinuxThrottleDevice(major: $0.major, minor: $0.minor, rate: $0.rate) + }, + throttleReadIOPSDevice: oci.throttleReadIOPSDevice.map { + Containerization.LinuxThrottleDevice(major: $0.major, minor: $0.minor, rate: $0.rate) + }, + throttleWriteIOPSDevice: oci.throttleWriteIOPSDevice.map { + Containerization.LinuxThrottleDevice(major: $0.major, minor: $0.minor, rate: $0.rate) + } + ) + } + private nonisolated func closeHandle(_ handle: Int32) throws { guard close(handle) == 0 else { guard let errCode = POSIXErrorCode(rawValue: errno) else { diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index dbb22c620..1f8b384b8 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1205,31 +1205,79 @@ struct ParserTest { let result = try Parser.resources( cpus: nil, memory: nil, - blkioWeight: 500, - blkioWeightDevice: ["/dev/null:700"], - deviceReadBps: ["/dev/null:1mb"], - deviceWriteBps: ["/dev/null:2mb"], - deviceReadIops: ["/dev/null:1000"], - deviceWriteIops: ["/dev/null:2000"], + blkio: [ + "weight=500,leaf-weight=300", + "device=/dev/null,weight=700,leaf-weight=400", + "device=/dev/null,read-bps=1mb,write-bps=2mb", + "device=/dev/null,read-iops=1000,write-iops=2000", + ], defaultCPUs: 8, defaultMemory: MemorySize("2g") ) let blockIO = try #require(result.blockIO) #expect(blockIO.weight == 500) + #expect(blockIO.leafWeight == 300) #expect(blockIO.weightDevice.first?.weight == 700) + #expect(blockIO.weightDevice.first?.leafWeight == 400) #expect(blockIO.throttleReadBpsDevice.first?.rate == 1.mib()) #expect(blockIO.throttleWriteBpsDevice.first?.rate == 2.mib()) #expect(blockIO.throttleReadIOPSDevice.first?.rate == 1000) #expect(blockIO.throttleWriteIOPSDevice.first?.rate == 2000) } + @Test func testResourcesBlockIOAcceptsMajorMinorLiteral() throws { + let result = try Parser.resources( + cpus: nil, + memory: nil, + blkio: ["device=8:0,weight=600,read-bps=512kb"], + defaultCPUs: 8, + defaultMemory: MemorySize("2g") + ) + + let blockIO = try #require(result.blockIO) + let weightDevice = try #require(blockIO.weightDevice.first) + #expect(weightDevice.major == 8) + #expect(weightDevice.minor == 0) + #expect(weightDevice.weight == 600) + #expect(blockIO.throttleReadBpsDevice.first?.rate == 512 * 1024) + } + @Test func testResourcesRejectsInvalidBlockIOWeight() throws { #expect { _ = try Parser.resources( cpus: nil, memory: nil, - blkioWeight: 1, + blkio: ["weight=1"], + defaultCPUs: 8, + defaultMemory: MemorySize("2g") + ) + } throws: { _ in + true + } + } + + @Test func testResourcesRejectsUnknownBlockIOKey() throws { + #expect { + _ = try Parser.resources( + cpus: nil, + memory: nil, + blkio: ["device=/dev/null,bogus=1"], + defaultCPUs: 8, + defaultMemory: MemorySize("2g") + ) + } throws: { _ in + true + } + } + + @Test func testResourcesRejectsGlobalKeyOnDeviceSpec() throws { + // read-bps without device= is meaningless. + #expect { + _ = try Parser.resources( + cpus: nil, + memory: nil, + blkio: ["read-bps=1mb"], defaultCPUs: 8, defaultMemory: MemorySize("2g") ) From e37400c1aead7044bf94f26764aabc63d7315b6a Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 27 May 2026 16:42:46 -0700 Subject: [PATCH 4/6] Address review: move blkio off ContainerConfiguration, into LinuxRuntimeData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses jglogan review feedback on PR #1595: 1. Move `blockIO` field out of the cross-platform `ContainerConfiguration.Resources` and into the Linux-specific `LinuxRuntimeData`. The CLI now encodes `LinuxRuntimeData(blockIO: …)` into the opaque `RuntimeConfiguration.runtimeData` field, and the Linux runtime decodes it inside `configureContainer` before applying the OCI `LinuxBlockIO` to `czConfig.blockIO`. Keeps OS-specific options out of the generic container config type. 2. Move the `--blkio` flag from `Flags.Resource` to `Flags.Management` and simplify its help to a single line pointing at the command reference, in the spirit of the existing generic options pattern. The structured key=value parsing/validation in `Parser.blockIO` is unchanged. 3. `Parser.resources` no longer takes `blkio`; `Parser.blockIO` stays public and is now invoked by `ContainerRun` / `ContainerCreate` directly. Tests rewritten to exercise `Parser.blockIO` directly. `swift build` clean; `swift test --filter ParserTest` 105 tests pass, `RuntimeConfiguration` tests pass, `container run --help` shows `--blkio` under MANAGEMENT OPTIONS. Deferred (per PR body): Package.swift / Package.resolved still pin containerization to apple/containerization#739's branch because that upstream PR is still open. Those will revert to apple/containerization at merge time, once #739 lands. --- Package.swift | 4 +- .../Container/ContainerCreate.swift | 11 +++- .../Container/ContainerRun.swift | 6 +- .../Container/ContainerConfiguration.swift | 2 - .../ContainerAPIService/Client/Flags.swift | 33 ++++------ .../ContainerAPIService/Client/Parser.swift | 3 - .../ContainerAPIService/Client/Utility.swift | 1 - .../Client/LinuxRuntimeData.swift | 7 +- .../RuntimeLinux/Server/RuntimeService.swift | 12 +++- .../ContainerAPIClientTests/ParserTest.swift | 66 +++++-------------- 10 files changed, 63 insertions(+), 82 deletions(-) diff --git a/Package.swift b/Package.swift index 2b1bb69b4..7464450e9 100644 --- a/Package.swift +++ b/Package.swift @@ -350,7 +350,9 @@ let package = Package( ), .target( name: "ContainerRuntimeLinuxClient", - dependencies: [], + dependencies: [ + .product(name: "ContainerizationOCI", package: "containerization"), + ], path: "Sources/Services/RuntimeLinux/Client" ), .executableTarget( diff --git a/Sources/ContainerCommands/Container/ContainerCreate.swift b/Sources/ContainerCommands/Container/ContainerCreate.swift index 92f0c02c1..3d6a0433e 100644 --- a/Sources/ContainerCommands/Container/ContainerCreate.swift +++ b/Sources/ContainerCommands/Container/ContainerCreate.swift @@ -19,6 +19,7 @@ import ContainerAPIClient import ContainerPersistence import ContainerPlugin import ContainerResource +import ContainerRuntimeLinuxClient import ContainerizationError import Foundation import TerminalProgress @@ -88,7 +89,15 @@ extension Application { let options = ContainerCreateOptions(autoRemove: managementFlags.remove) let client = ContainerClient() - try await client.create(configuration: ck.0, options: options, kernel: ck.1, initImage: ck.2) + let blockIO = try Parser.blockIO(specs: managementFlags.blkio) + let runtimeData: Data? = try blockIO.map { try JSONEncoder().encode(LinuxRuntimeData(blockIO: $0)) } + try await client.create( + configuration: ck.0, + options: options, + kernel: ck.1, + initImage: ck.2, + runtimeData: runtimeData + ) if !self.managementFlags.cidfile.isEmpty { let path = self.managementFlags.cidfile diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index 431279ac5..0d39409d6 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -19,6 +19,7 @@ import ContainerAPIClient import ContainerPersistence import ContainerPlugin import ContainerResource +import ContainerRuntimeLinuxClient import Containerization import ContainerizationError import ContainerizationExtras @@ -109,11 +110,14 @@ extension Application { progress.set(description: "Starting container") let options = ContainerCreateOptions(autoRemove: managementFlags.remove) + let blockIO = try Parser.blockIO(specs: managementFlags.blkio) + let runtimeData: Data? = try blockIO.map { try JSONEncoder().encode(LinuxRuntimeData(blockIO: $0)) } try await client.create( configuration: ck.0, options: options, kernel: ck.1, - initImage: ck.2 + initImage: ck.2, + runtimeData: runtimeData ) let detach = self.managementFlags.detach diff --git a/Sources/ContainerResource/Container/ContainerConfiguration.swift b/Sources/ContainerResource/Container/ContainerConfiguration.swift index ec31e5e15..b1f234915 100644 --- a/Sources/ContainerResource/Container/ContainerConfiguration.swift +++ b/Sources/ContainerResource/Container/ContainerConfiguration.swift @@ -154,8 +154,6 @@ public struct ContainerConfiguration: Sendable, Codable { public var cpus: Int = 4 /// Memory in bytes allocated. public var memoryInBytes: UInt64 = 1024.mib() - /// Block I/O resource limits. - public var blockIO: LinuxBlockIO? /// Storage quota/size in bytes. public var storage: UInt64? /// Additional CPU cores allocated for VM overhead (guest agent, etc). diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index e78603206..3346ce2de 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -103,12 +103,10 @@ public struct Flags { public init( cpus: Int64?, - memory: String?, - blkio: [String] = [] + memory: String? ) { self.cpus = cpus self.memory = memory - self.blkio = blkio } @Option(name: .shortAndLong, help: "Number of CPUs to allocate to the container") @@ -119,24 +117,6 @@ public struct Flags { help: "Amount of memory (1MiByte granularity), with optional K, M, G, T, or P suffix" ) public var memory: String? - - @Option( - name: .customLong("blkio"), - help: .init( - """ - Block I/O cgroup tuning (repeatable). Comma-separated key=value pairs: - weight=<10..1000> cgroup-wide weight - leaf-weight=<10..1000> cgroup-wide leaf weight - device=,weight=... per-device weight - device=<...>,read-bps= per-device read throughput limit - device=<...>,write-bps= per-device write throughput limit - device=<...>,read-iops= per-device read IOPS limit - device=<...>,write-iops= per-device write IOPS limit - """, - valueName: "spec" - ) - ) - public var blkio: [String] = [] } public struct DNS: ParsableArguments { @@ -213,6 +193,7 @@ public struct Flags { runtime: String?, ssh: Bool, shmSize: String?, + blkio: [String] = [], tmpFs: [String], useInit: Bool, virtualization: Bool, @@ -242,6 +223,7 @@ public struct Flags { self.runtime = runtime self.ssh = ssh self.shmSize = shmSize + self.blkio = blkio self.tmpFs = tmpFs self.useInit = useInit self.virtualization = virtualization @@ -357,6 +339,15 @@ public struct Flags { @Option(name: .customLong("shm-size"), help: "Size of /dev/shm (e.g. 64M, 1G)") public var shmSize: String? + @Option( + name: .customLong("blkio"), + help: .init( + "Block I/O cgroup tuning options (Linux only; see command reference for the supported keys)", + valueName: "option" + ) + ) + public var blkio: [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/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index ec922974a..126dc0e51 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -98,7 +98,6 @@ public struct Parser { public static func resources( cpus: Int64?, memory: String?, - blkio: [String] = [], defaultCPUs: Int, defaultMemory: MemorySize, ) throws -> ContainerConfiguration.Resources { @@ -114,8 +113,6 @@ public struct Parser { resource.memoryInBytes = try Parser.memoryStringAsMiB(memory).mib() } - resource.blockIO = try Parser.blockIO(specs: blkio) - return resource } diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index 559fc5761..bfea2bbc0 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -159,7 +159,6 @@ public struct Utility { config.resources = try Parser.resources( cpus: resource.cpus, memory: resource.memory, - blkio: resource.blkio, defaultCPUs: containerSystemConfig.container.cpus, defaultMemory: containerSystemConfig.container.memory ) diff --git a/Sources/Services/RuntimeLinux/Client/LinuxRuntimeData.swift b/Sources/Services/RuntimeLinux/Client/LinuxRuntimeData.swift index d30185a11..017303d48 100644 --- a/Sources/Services/RuntimeLinux/Client/LinuxRuntimeData.swift +++ b/Sources/Services/RuntimeLinux/Client/LinuxRuntimeData.swift @@ -14,14 +14,19 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerizationOCI import Foundation /// Linux-specific runtime data passed through the opaque runtimeData field /// in RuntimeConfiguration. Encoded by the CLI, decoded by the Linux runtime. public struct LinuxRuntimeData: Codable, Sendable { public let variant: String? + /// Block I/O cgroup tuning (Linux-specific, carried opaquely through + /// `RuntimeConfiguration.runtimeData`). + public let blockIO: ContainerizationOCI.LinuxBlockIO? - public init(variant: String? = nil) { + public init(variant: String? = nil, blockIO: ContainerizationOCI.LinuxBlockIO? = nil) { self.variant = variant + self.blockIO = blockIO } } diff --git a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift index ee8a5e1a4..55b60ab75 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -19,6 +19,7 @@ import ContainerOS import ContainerPersistence import ContainerResource import ContainerRuntimeClient +import ContainerRuntimeLinuxClient import ContainerXPC import Containerization import ContainerizationError @@ -158,6 +159,9 @@ public actor RuntimeService { try bundle.createLogFile() var config = try bundle.configuration + // Pick up Linux-specific runtime data (e.g. blkio cgroup tuning) carried opaquely + // through `RuntimeConfiguration.runtimeData`. + let runtimeData: Data? = (try? RuntimeConfiguration.readRuntimeConfiguration(from: self.root))?.runtimeData var kernel = try bundle.kernel kernel.commandLine.kernelArgs.append("oops=panic") @@ -253,7 +257,7 @@ public actor RuntimeService { let id = config.id let rootfs = try bundle.containerRootfs.asMount let container = try LinuxContainer(id, rootfs: rootfs, vmm: vmm, logger: self.log) { czConfig in - try Self.configureContainer(czConfig: &czConfig, config: config, dynamicEnv: dynamicEnv, log: self.log) + try Self.configureContainer(czConfig: &czConfig, config: config, runtimeData: runtimeData, dynamicEnv: dynamicEnv, log: self.log) czConfig.interfaces = interfaces czConfig.process.stdout = stdout czConfig.process.stderr = stderr @@ -980,13 +984,17 @@ public actor RuntimeService { private static func configureContainer( czConfig: inout LinuxContainer.Configuration, config: ContainerConfiguration, + runtimeData: Data? = nil, dynamicEnv: [String: String] = [:], log: Logger? = nil, ) throws { czConfig.cpus = config.resources.cpus czConfig.cpuOverhead = config.resources.cpuOverhead czConfig.memoryInBytes = config.resources.memoryInBytes - czConfig.blockIO = config.resources.blockIO.map(Self.toContainerizationBlockIO) + if let runtimeData { + let linuxData = try JSONDecoder().decode(LinuxRuntimeData.self, from: runtimeData) + czConfig.blockIO = linuxData.blockIO.map(Self.toContainerizationBlockIO) + } czConfig.sysctl = config.sysctls.reduce(into: [String: String]()) { $0[$1.key] = $1.value } diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 1f8b384b8..49a544770 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -1201,21 +1201,14 @@ struct ParserTest { #expect(result.memoryInBytes == 256.mib()) } - @Test func testResourcesBlockIOFlags() throws { - let result = try Parser.resources( - cpus: nil, - memory: nil, - blkio: [ - "weight=500,leaf-weight=300", - "device=/dev/null,weight=700,leaf-weight=400", - "device=/dev/null,read-bps=1mb,write-bps=2mb", - "device=/dev/null,read-iops=1000,write-iops=2000", - ], - defaultCPUs: 8, - defaultMemory: MemorySize("2g") - ) - - let blockIO = try #require(result.blockIO) + @Test func testBlockIOSpecsCombined() throws { + let parsed = try Parser.blockIO(specs: [ + "weight=500,leaf-weight=300", + "device=/dev/null,weight=700,leaf-weight=400", + "device=/dev/null,read-bps=1mb,write-bps=2mb", + "device=/dev/null,read-iops=1000,write-iops=2000", + ]) + let blockIO = try #require(parsed) #expect(blockIO.weight == 500) #expect(blockIO.leafWeight == 300) #expect(blockIO.weightDevice.first?.weight == 700) @@ -1226,16 +1219,9 @@ struct ParserTest { #expect(blockIO.throttleWriteIOPSDevice.first?.rate == 2000) } - @Test func testResourcesBlockIOAcceptsMajorMinorLiteral() throws { - let result = try Parser.resources( - cpus: nil, - memory: nil, - blkio: ["device=8:0,weight=600,read-bps=512kb"], - defaultCPUs: 8, - defaultMemory: MemorySize("2g") - ) - - let blockIO = try #require(result.blockIO) + @Test func testBlockIOAcceptsMajorMinorLiteral() throws { + let parsed = try Parser.blockIO(specs: ["device=8:0,weight=600,read-bps=512kb"]) + let blockIO = try #require(parsed) let weightDevice = try #require(blockIO.weightDevice.first) #expect(weightDevice.major == 8) #expect(weightDevice.minor == 0) @@ -1243,44 +1229,26 @@ struct ParserTest { #expect(blockIO.throttleReadBpsDevice.first?.rate == 512 * 1024) } - @Test func testResourcesRejectsInvalidBlockIOWeight() throws { + @Test func testBlockIORejectsInvalidWeight() throws { #expect { - _ = try Parser.resources( - cpus: nil, - memory: nil, - blkio: ["weight=1"], - defaultCPUs: 8, - defaultMemory: MemorySize("2g") - ) + _ = try Parser.blockIO(specs: ["weight=1"]) } throws: { _ in true } } - @Test func testResourcesRejectsUnknownBlockIOKey() throws { + @Test func testBlockIORejectsUnknownKey() throws { #expect { - _ = try Parser.resources( - cpus: nil, - memory: nil, - blkio: ["device=/dev/null,bogus=1"], - defaultCPUs: 8, - defaultMemory: MemorySize("2g") - ) + _ = try Parser.blockIO(specs: ["device=/dev/null,bogus=1"]) } throws: { _ in true } } - @Test func testResourcesRejectsGlobalKeyOnDeviceSpec() throws { + @Test func testBlockIORejectsGlobalKeyOnDeviceSpec() throws { // read-bps without device= is meaningless. #expect { - _ = try Parser.resources( - cpus: nil, - memory: nil, - blkio: ["read-bps=1mb"], - defaultCPUs: 8, - defaultMemory: MemorySize("2g") - ) + _ = try Parser.blockIO(specs: ["read-bps=1mb"]) } throws: { _ in true } From 566ed39b4bcb08e58a790e68450e452ce1f13e74 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 27 May 2026 17:03:14 -0700 Subject: [PATCH 5/6] Revert containerization pin back to apple/containerization 0.33.2 The branch pin to full-chaos/containerization@feat/chaos-1380-blkio-runtime was a temporary measure while apple/containerization#739 was in flight. Revert to the upstream pin so this PR can be merged independently of #739. Note: the runtime plumbing in RuntimeService.swift still references Containerization.LinuxBlockIO and czConfig.blockIO, which only exist on the #739 branch. The build will be temporarily broken until #739 lands upstream and the pin is bumped to whatever release contains it. --- Package.resolved | 3 +-- Package.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index c68b51a18..70a905580 100644 --- a/Package.resolved +++ b/Package.resolved @@ -13,11 +13,10 @@ { "identity" : "containerization", "kind" : "remoteSourceControl", - "location" : "https://github.com/full-chaos/containerization.git", + "location" : "https://github.com/apple/containerization.git", "state" : { "revision" : "9275f365dd555c8f072e7d250d809f5eb7bdd746", "version" : "0.33.4" - } }, { diff --git a/Package.swift b/Package.swift index 7464450e9..b2475be37 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,7 @@ let package = Package( .library(name: "MachineAPIService", targets: ["MachineAPIService"]), ], dependencies: [ - .package(url: "https://github.com/full-chaos/containerization.git", branch: scVersion), + .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), .package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"), From bf98d312000805a162e48623e3c5be871b9bb59d Mon Sep 17 00:00:00 2001 From: Chris George Date: Thu, 28 May 2026 19:22:40 -0700 Subject: [PATCH 6/6] Update Sources/Services/ContainerAPIService/Client/Flags.swift Co-authored-by: J Logan --- Sources/Services/ContainerAPIService/Client/Flags.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index 3346ce2de..46b77d62f 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -342,7 +342,7 @@ public struct Flags { @Option( name: .customLong("blkio"), help: .init( - "Block I/O cgroup tuning options (Linux only; see command reference for the supported keys)", + "Block I/O cgroup tuning options (experimental: see command reference for the supported keys)", valueName: "option" ) )