Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions Sources/Containerization/LinuxBlockIO.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
9 changes: 7 additions & 2 deletions Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we don't use the OCI types directly (it gives us a little wiggle room to add various knobs). You can add a new LinuxBlockIO type in Containerization and use this and just convert to the oci variant like we do for other things in this file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback.

Does this new shape align with what you were imagining?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dcantah @chrisgeo see apple/container#1512 (comment) for thoughts on container UX as well.

/// The hostname for the container.
public var hostname: String?
/// The system control options for the container.
Expand Down Expand Up @@ -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] = [],
Expand All @@ -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
Expand Down Expand Up @@ -374,7 +378,7 @@ public final class LinuxContainer: Container, Sendable {
)
}

private func generateRuntimeSpec() -> Spec {
func generateRuntimeSpec() -> Spec {
var spec = Self.createDefaultRuntimeSpec(id)

// Process toggles.
Expand Down Expand Up @@ -409,7 +413,8 @@ public final class LinuxContainer: Container, Sendable {
cpu: LinuxCPU(
quota: Int64(config.cpus * 100_000),
period: 100_000
)
),
blockIO: config.blockIO?.toOCI()
)

spec.linux?.namespaces = [
Expand Down
52 changes: 51 additions & 1 deletion Tests/ContainerizationTests/LinuxContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -66,4 +67,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")
}
}