From 53fd7c3f265d00e6e02c8018b3d62b858a8a45ad Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 6 May 2026 09:32:38 -0700 Subject: [PATCH 1/3] 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 34964fcdc..b2fcb19d6 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -57,6 +57,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. @@ -105,6 +107,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] = [], @@ -124,6 +127,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 @@ -389,7 +393,7 @@ public final class LinuxContainer: Container, Sendable { ) } - private func generateRuntimeSpec() -> Spec { + func generateRuntimeSpec() -> Spec { var spec = Self.createDefaultRuntimeSpec(id) // Process toggles. @@ -426,7 +430,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 713e3982d..9db6f5743 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -119,4 +119,53 @@ struct LinuxContainerTests { #expect(pod.maskedPaths == expectedMasked) #expect(pod.readonlyPaths == expectedReadonly) } + + @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 9488c92031d27db84480aee7b76b22ad71e2f3c5 Mon Sep 17 00:00:00 2001 From: Chris George Date: Thu, 14 May 2026 15:13:59 -0700 Subject: [PATCH 2/3] 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 | 2 + 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 Sources/Containerization/LinuxBlockIO.swift diff --git a/Sources/Containerization/LinuxBlockIO.swift b/Sources/Containerization/LinuxBlockIO.swift new file mode 100644 index 000000000..486be9b05 --- /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 b2fcb19d6..8ab69ddf5 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -431,7 +431,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 9db6f5743..4a94ae06d 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -21,6 +21,8 @@ import Testing @testable import Containerization +import struct ContainerizationOCI.ImageConfig + struct LinuxContainerTests { @Test func processInitFromImageConfigWithAllFields() { From 63527275f9ad41e13984038e155f13f056a75def Mon Sep 17 00:00:00 2001 From: Chris George Date: Mon, 1 Jun 2026 08:47:30 -0700 Subject: [PATCH 3/3] Fix formatting and license header for block I/O changes --- Sources/Containerization/LinuxBlockIO.swift | 2 +- .../LinuxContainerTests.swift | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Containerization/LinuxBlockIO.swift b/Sources/Containerization/LinuxBlockIO.swift index 486be9b05..e6858040e 100644 --- a/Sources/Containerization/LinuxBlockIO.swift +++ b/Sources/Containerization/LinuxBlockIO.swift @@ -1,5 +1,5 @@ //===----------------------------------------------------------------------===// -// Copyright © 2025-2026 Apple Inc. and the Containerization project authors. +// Copyright © 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. diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index 4a94ae06d..f3a3f792a 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -19,10 +19,10 @@ import ContainerizationOS import Foundation import Testing -@testable import Containerization - import struct ContainerizationOCI.ImageConfig +@testable import Containerization + struct LinuxContainerTests { @Test func processInitFromImageConfigWithAllFields() { @@ -123,23 +123,23 @@ struct LinuxContainerTests { } @Test func runtimeSpecIncludesConfiguredBlockIO() throws { - let blockIO = LinuxBlockIO( + let blockIO = Containerization.LinuxBlockIO( weight: 500, leafWeight: 300, weightDevice: [ - LinuxWeightDevice(major: 8, minor: 0, weight: 700, leafWeight: 400) + Containerization.LinuxWeightDevice(major: 8, minor: 0, weight: 700, leafWeight: 400) ], throttleReadBpsDevice: [ - LinuxThrottleDevice(major: 8, minor: 16, rate: 1_048_576) + Containerization.LinuxThrottleDevice(major: 8, minor: 16, rate: 1_048_576) ], throttleWriteBpsDevice: [ - LinuxThrottleDevice(major: 8, minor: 32, rate: 2_097_152) + Containerization.LinuxThrottleDevice(major: 8, minor: 32, rate: 2_097_152) ], throttleReadIOPSDevice: [ - LinuxThrottleDevice(major: 8, minor: 48, rate: 1_000) + Containerization.LinuxThrottleDevice(major: 8, minor: 48, rate: 1_000) ], throttleWriteIOPSDevice: [ - LinuxThrottleDevice(major: 8, minor: 64, rate: 2_000) + Containerization.LinuxThrottleDevice(major: 8, minor: 64, rate: 2_000) ] )