From 04997956b5f82b2f8d49a7887b037c1d97cceeae Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 6 May 2026 09:41:38 -0700 Subject: [PATCH 1/5] Add blkio resource flags --- Package.resolved | 6 +- Package.swift | 4 +- .../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, 214 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 6d19fb55d..b499eb524 100644 --- a/Package.resolved +++ b/Package.resolved @@ -13,10 +13,10 @@ { "identity" : "containerization", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/containerization.git", + "location" : "https://github.com/full-chaos/containerization.git", "state" : { - "revision" : "9205a766302a18e06a0ce43f8b7e483625e3e50f", - "version" : "0.33.1" + "branch" : "feat/chaos-1380-blkio-runtime", + "revision" : "14dec0ed11d7f25a1c4618b778354c8391afeab4" } }, { diff --git a/Package.swift b/Package.swift index 2cc2e7092..91c819280 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ import PackageDescription let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0" let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified" let builderShimVersion = "0.12.0" -let scVersion = "0.33.1" +let scVersion = "feat/chaos-1380-blkio-runtime" let package = Package( name: "container", @@ -49,7 +49,7 @@ let package = Package( .library(name: "TerminalProgress", targets: ["TerminalProgress"]), ], 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 4dc75b9ad..7fada01f8 100644 --- a/Sources/ContainerResource/Container/ContainerConfiguration.swift +++ b/Sources/ContainerResource/Container/ContainerConfiguration.swift @@ -149,6 +149,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? diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index ec7b83b40..e94f21a78 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 7cbe7e0b0..ccba2f622 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 /// A parsed volume specification from user input @@ -90,6 +91,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 { @@ -105,9 +112,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 f649b68aa..90bd28605 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -160,6 +160,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 187de7af6..62f14281d 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -970,6 +970,7 @@ public actor RuntimeService { ) throws { czConfig.cpus = config.resources.cpus 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 53d8426b6d2d7a55d9b1c40141c8a3174582f7ea Mon Sep 17 00:00:00 2001 From: Chris George Date: Sat, 23 May 2026 17:23:02 -0700 Subject: [PATCH 2/5] 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 e94f21a78..52b63145b 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 ccba2f622..bac0e0f21 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -91,12 +91,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 { @@ -112,85 +107,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 { @@ -204,14 +287,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 90bd28605..b818efc4a 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -160,12 +160,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 62f14281d..ffa9fd91d 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -970,7 +970,7 @@ public actor RuntimeService { ) throws { czConfig.cpus = config.resources.cpus 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 } @@ -1176,6 +1176,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 b33c3a631e46945c593cf7fdae76508e89c87b21 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 27 May 2026 16:42:46 -0700 Subject: [PATCH 3/5] 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 91c819280..8a3dba68d 100644 --- a/Package.swift +++ b/Package.swift @@ -341,7 +341,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 75832bd79..3a72795ed 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 7fada01f8..4dc75b9ad 100644 --- a/Sources/ContainerResource/Container/ContainerConfiguration.swift +++ b/Sources/ContainerResource/Container/ContainerConfiguration.swift @@ -149,8 +149,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? diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index 52b63145b..1ad6701f2 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 bac0e0f21..2068e303d 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -91,7 +91,6 @@ public struct Parser { public static func resources( cpus: Int64?, memory: String?, - blkio: [String] = [], defaultCPUs: Int, defaultMemory: MemorySize, ) throws -> ContainerConfiguration.Resources { @@ -107,8 +106,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 b818efc4a..f649b68aa 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -160,7 +160,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 ffa9fd91d..3c689334d 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") @@ -252,7 +256,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 @@ -965,12 +969,16 @@ 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.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 fb6ca810b6c9be529ad2f5d61eb2d61f65795bc1 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 27 May 2026 17:03:14 -0700 Subject: [PATCH 4/5] 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 | 6 +++--- Package.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index b499eb524..ad996952e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -13,10 +13,10 @@ { "identity" : "containerization", "kind" : "remoteSourceControl", - "location" : "https://github.com/full-chaos/containerization.git", + "location" : "https://github.com/apple/containerization.git", "state" : { - "branch" : "feat/chaos-1380-blkio-runtime", - "revision" : "14dec0ed11d7f25a1c4618b778354c8391afeab4" + "revision" : "2550dd49f1890702f6fe0171212050bbce9d3825", + "version" : "0.33.2" } }, { diff --git a/Package.swift b/Package.swift index 8a3dba68d..6ce90d8ba 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ import PackageDescription let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0" let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified" let builderShimVersion = "0.12.0" -let scVersion = "feat/chaos-1380-blkio-runtime" +let scVersion = "0.33.2" let package = Package( name: "container", @@ -49,7 +49,7 @@ let package = Package( .library(name: "TerminalProgress", targets: ["TerminalProgress"]), ], 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 9736b246446b55d7da8aab37e69c247c37792aef Mon Sep 17 00:00:00 2001 From: Chris George Date: Thu, 28 May 2026 19:22:40 -0700 Subject: [PATCH 5/5] 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 1ad6701f2..b00b46d00 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" ) )