From 5268c48d6a82c5a61e7a73da4c9b3a165cdd8f53 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 6 May 2026 09:32:38 -0700 Subject: [PATCH 1/2] Add LinuxContainer block I/O resources --- Sources/Containerization/LinuxContainer.swift | 9 +++- .../LinuxContainerTests.swift | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 18f51e51..d011b52b 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -56,6 +56,8 @@ public final class LinuxContainer: Container, Sendable { public var cpus: Int = 4 /// The memory in bytes to give to the container. public var memoryInBytes: UInt64 = 1024.mib() + /// Optional block I/O resource limits for the container cgroup. + public var blockIO: LinuxBlockIO? /// The hostname for the container. public var hostname: String? /// The system control options for the container. @@ -94,6 +96,7 @@ public final class LinuxContainer: Container, Sendable { process: LinuxProcessConfiguration, cpus: Int = 4, memoryInBytes: UInt64 = 1024.mib(), + blockIO: LinuxBlockIO? = nil, hostname: String? = nil, sysctl: [String: String] = [:], interfaces: [any Interface] = [], @@ -111,6 +114,7 @@ public final class LinuxContainer: Container, Sendable { self.process = process self.cpus = cpus self.memoryInBytes = memoryInBytes + self.blockIO = blockIO self.hostname = hostname self.sysctl = sysctl self.interfaces = interfaces @@ -374,7 +378,7 @@ public final class LinuxContainer: Container, Sendable { ) } - private func generateRuntimeSpec() -> Spec { + func generateRuntimeSpec() -> Spec { var spec = Self.createDefaultRuntimeSpec(id) // Process toggles. @@ -409,7 +413,8 @@ public final class LinuxContainer: Container, Sendable { cpu: LinuxCPU( quota: Int64(config.cpus * 100_000), period: 100_000 - ) + ), + blockIO: config.blockIO ) spec.linux?.namespaces = [ diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index ddf4a678..a0a8b622 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -66,4 +66,53 @@ struct LinuxContainerTests { #expect(process.arguments == ["/bin/sh", "-c", "echo 'hello'", "&&", "sleep 10"]) } + + @Test func runtimeSpecIncludesConfiguredBlockIO() throws { + let blockIO = LinuxBlockIO( + weight: 500, + leafWeight: 300, + weightDevice: [ + LinuxWeightDevice(major: 8, minor: 0, weight: 700, leafWeight: 400) + ], + throttleReadBpsDevice: [ + LinuxThrottleDevice(major: 8, minor: 16, rate: 1_048_576) + ], + throttleWriteBpsDevice: [ + LinuxThrottleDevice(major: 8, minor: 32, rate: 2_097_152) + ], + throttleReadIOPSDevice: [ + LinuxThrottleDevice(major: 8, minor: 48, rate: 1_000) + ], + throttleWriteIOPSDevice: [ + LinuxThrottleDevice(major: 8, minor: 64, rate: 2_000) + ] + ) + + let container = try LinuxContainer( + "blkio-test", + rootfs: .block(format: "ext4", source: "/tmp/rootfs.img", destination: "/"), + vmm: StubVirtualMachineManager(), + configuration: .init(process: .init(), blockIO: blockIO) + ) + + let resources = try #require(container.generateRuntimeSpec().linux?.resources) + let specBlockIO = try #require(resources.blockIO) + + #expect(specBlockIO.weight == 500) + #expect(specBlockIO.leafWeight == 300) + #expect(specBlockIO.weightDevice.first?.major == 8) + #expect(specBlockIO.weightDevice.first?.minor == 0) + #expect(specBlockIO.weightDevice.first?.weight == 700) + #expect(specBlockIO.weightDevice.first?.leafWeight == 400) + #expect(specBlockIO.throttleReadBpsDevice.first?.rate == 1_048_576) + #expect(specBlockIO.throttleWriteBpsDevice.first?.rate == 2_097_152) + #expect(specBlockIO.throttleReadIOPSDevice.first?.rate == 1_000) + #expect(specBlockIO.throttleWriteIOPSDevice.first?.rate == 2_000) + } +} + +private struct StubVirtualMachineManager: VirtualMachineManager { + func create(config: some VMCreationConfig) async throws -> any VirtualMachineInstance { + fatalError("StubVirtualMachineManager.create should not be called by LinuxContainerTests") + } } From 3d009dfc2724fc924dd18ca2014b5e839d554163 Mon Sep 17 00:00:00 2001 From: Chris George Date: Thu, 14 May 2026 15:13:59 -0700 Subject: [PATCH 2/2] Wrap LinuxBlockIO with a Containerization type Mirrors the LinuxRLimit/LinuxCapabilities pattern so the public API can evolve independently of the OCI spec types. Configuration.blockIO now holds the wrapper and is converted via toOCI() at spec assembly. --- Sources/Containerization/LinuxBlockIO.swift | 125 ++++++++++++++++++ Sources/Containerization/LinuxContainer.swift | 2 +- .../LinuxContainerTests.swift | 3 +- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 Sources/Containerization/LinuxBlockIO.swift diff --git a/Sources/Containerization/LinuxBlockIO.swift b/Sources/Containerization/LinuxBlockIO.swift new file mode 100644 index 00000000..486be9b0 --- /dev/null +++ b/Sources/Containerization/LinuxBlockIO.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025-2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationOCI + +/// Block I/O resource limits applied to the container cgroup. +public struct LinuxBlockIO: Sendable { + /// The relative weight of the cgroup for block I/O. Valid range is 10 to 1000. + public var weight: UInt16? + /// The relative weight applied to tasks of the cgroup but not their descendant cgroups. + public var leafWeight: UInt16? + /// Per-device weight overrides. + public var weightDevice: [LinuxWeightDevice] + /// Per-device read rate limits in bytes per second. + public var throttleReadBpsDevice: [LinuxThrottleDevice] + /// Per-device write rate limits in bytes per second. + public var throttleWriteBpsDevice: [LinuxThrottleDevice] + /// Per-device read rate limits in IO operations per second. + public var throttleReadIOPSDevice: [LinuxThrottleDevice] + /// Per-device write rate limits in IO operations per second. + public var throttleWriteIOPSDevice: [LinuxThrottleDevice] + + public init( + weight: UInt16? = nil, + leafWeight: UInt16? = nil, + weightDevice: [LinuxWeightDevice] = [], + throttleReadBpsDevice: [LinuxThrottleDevice] = [], + throttleWriteBpsDevice: [LinuxThrottleDevice] = [], + throttleReadIOPSDevice: [LinuxThrottleDevice] = [], + throttleWriteIOPSDevice: [LinuxThrottleDevice] = [] + ) { + self.weight = weight + self.leafWeight = leafWeight + self.weightDevice = weightDevice + self.throttleReadBpsDevice = throttleReadBpsDevice + self.throttleWriteBpsDevice = throttleWriteBpsDevice + self.throttleReadIOPSDevice = throttleReadIOPSDevice + self.throttleWriteIOPSDevice = throttleWriteIOPSDevice + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxBlockIO { + ContainerizationOCI.LinuxBlockIO( + weight: self.weight, + leafWeight: self.leafWeight, + weightDevice: self.weightDevice.map { $0.toOCI() }, + throttleReadBpsDevice: self.throttleReadBpsDevice.map { $0.toOCI() }, + throttleWriteBpsDevice: self.throttleWriteBpsDevice.map { $0.toOCI() }, + throttleReadIOPSDevice: self.throttleReadIOPSDevice.map { $0.toOCI() }, + throttleWriteIOPSDevice: self.throttleWriteIOPSDevice.map { $0.toOCI() } + ) + } +} + +/// A per-device block I/O weight override. +public struct LinuxWeightDevice: Sendable { + /// The major device number. + public var major: Int64 + /// The minor device number. + public var minor: Int64 + /// The relative weight applied to the device. Valid range is 10 to 1000. + public var weight: UInt16? + /// The relative weight applied to tasks of the cgroup but not their descendant cgroups. + public var leafWeight: UInt16? + + public init( + major: Int64, + minor: Int64, + weight: UInt16? = nil, + leafWeight: UInt16? = nil + ) { + self.major = major + self.minor = minor + self.weight = weight + self.leafWeight = leafWeight + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxWeightDevice { + ContainerizationOCI.LinuxWeightDevice( + major: self.major, + minor: self.minor, + weight: self.weight, + leafWeight: self.leafWeight + ) + } +} + +/// A per-device block I/O throughput limit. +public struct LinuxThrottleDevice: Sendable { + /// The major device number. + public var major: Int64 + /// The minor device number. + public var minor: Int64 + /// The rate limit applied to the device. + public var rate: UInt64 + + public init(major: Int64, minor: Int64, rate: UInt64) { + self.major = major + self.minor = minor + self.rate = rate + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxThrottleDevice { + ContainerizationOCI.LinuxThrottleDevice( + major: self.major, + minor: self.minor, + rate: self.rate + ) + } +} diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index d011b52b..7736f72a 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -414,7 +414,7 @@ public final class LinuxContainer: Container, Sendable { quota: Int64(config.cpus * 100_000), period: 100_000 ), - blockIO: config.blockIO + blockIO: config.blockIO?.toOCI() ) spec.linux?.namespaces = [ diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index a0a8b622..80bcf704 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -14,12 +14,13 @@ // limitations under the License. //===----------------------------------------------------------------------===// -import ContainerizationOCI import Foundation import Testing @testable import Containerization +import struct ContainerizationOCI.ImageConfig + struct LinuxContainerTests { @Test func processInitFromImageConfigWithAllFields() {