Skip to content

Commit 3865df5

Browse files
authored
Merge branch 'main' into archive-urls-oss
2 parents 8ce3ebf + 6c827ee commit 3865df5

30 files changed

Lines changed: 3613 additions & 318 deletions

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,11 @@ endif
123123
.PHONY: init
124124
init: containerization vminitd
125125
@echo Creating init.ext4...
126-
@rm -f bin/init.rootfs.tar.gz bin/init.block
126+
@rm -f bin/init.rootfs.tar.gz bin/init.block bin/initfs.ext4
127127
@./bin/cctl rootfs create \
128128
--vminitd vminitd/bin/vminitd \
129129
--vmexec vminitd/bin/vmexec \
130+
--ext4 ./bin/initfs.ext4 \
130131
--label org.opencontainers.image.source=https://github.com/apple/containerization \
131132
--image vminit:latest \
132133
bin/init.rootfs.tar.gz

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ package.targets.append(
269269
dependencies: [
270270
.product(name: "Logging", package: "swift-log"),
271271
.product(name: "ArgumentParser", package: "swift-argument-parser"),
272+
.product(name: "NIOCore", package: "swift-nio"),
273+
.product(name: "NIOPosix", package: "swift-nio"),
272274
"Containerization",
273275
],
274276
path: "Sources/Integration"

Sources/Containerization/AttachedFilesystem.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ public struct AttachedFilesystem: Sendable {
2929
public var options: [String]
3030

3131
public init(mount: Mount, allocator: any AddressAllocator<Character>) throws {
32-
switch mount.type {
33-
case "virtiofs":
32+
switch mount.runtimeOptions {
33+
case .virtiofs:
3434
let name = try hashMountSource(source: mount.source)
3535
self.source = name
36-
case "ext4":
36+
case .virtioblk:
3737
let char = try allocator.allocate()
3838
self.source = "/dev/vd\(char)"
39-
default:
39+
case .shared, .any:
4040
self.source = mount.source
4141
}
4242
self.type = mount.type
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerizationError
18+
import GRPCCore
19+
import GRPCNIOTransportCore
20+
import NIOCore
21+
import NIOPosix
22+
23+
/// Buffers incoming bytes until the full gRPC HTTP/2 pipeline is configured, then replays them.
24+
///
25+
/// This prevents the race condition where the vminitd server's initial HTTP/2 SETTINGS frame
26+
/// arrives and is discarded before `configureGRPCClientPipeline` has finished installing
27+
/// `ClientConnectionHandler`.
28+
///
29+
/// The handler is added via `ClientBootstrap.channelInitializer`, which runs before
30+
/// `registerAlreadyConfigured0` adds the fd to epoll/kqueue — guaranteeing it is in place
31+
/// before any bytes can arrive on the socket.
32+
///
33+
/// When `NIOHTTP2Handler` is added to the pipeline (inside `configureGRPCClientPipeline`), its
34+
/// `handlerAdded` fires an outbound flush (the HTTP/2 client preface). We intercept that flush
35+
/// and schedule a deferred removal via the event loop. Because `configureGRPCClientPipeline` runs
36+
/// as a single synchronous event loop task, the deferred removal is guaranteed to run after that
37+
/// entire task completes — i.e., after `ClientConnectionHandler` is also in the pipeline.
38+
/// Buffered bytes are replayed atomically as part of the pipeline removal.
39+
40+
// FIXME: This handler is needed until the swift GRPC libraries offers us a way to create a
41+
// client transport from an existing fd. Remove this type when such an API exists.
42+
public final class HTTP2ConnectBufferingHandler: ChannelDuplexHandler, RemovableChannelHandler {
43+
public typealias InboundIn = ByteBuffer
44+
public typealias InboundOut = ByteBuffer
45+
public typealias OutboundIn = ByteBuffer
46+
public typealias OutboundOut = ByteBuffer
47+
48+
private var removalScheduled = false
49+
private var bufferedReads: [NIOAny] = []
50+
51+
public init() {}
52+
53+
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
54+
bufferedReads.append(data)
55+
}
56+
57+
public func channelReadComplete(context: ChannelHandlerContext) {
58+
// Suppress while buffering; a single readComplete is emitted after replay.
59+
}
60+
61+
public func flush(context: ChannelHandlerContext) {
62+
if !removalScheduled {
63+
removalScheduled = true
64+
// Defer removal to the next event loop task. configureGRPCClientPipeline runs as a
65+
// single synchronous event loop task, so this deferred task is guaranteed to run
66+
// after that whole task completes (including ClientConnectionHandler being added).
67+
context.eventLoop.assumeIsolatedUnsafeUnchecked().execute {
68+
context.pipeline.syncOperations.removeHandler(self, promise: nil)
69+
}
70+
}
71+
context.flush()
72+
}
73+
74+
public func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
75+
var didRead = false
76+
while !bufferedReads.isEmpty {
77+
context.fireChannelRead(bufferedReads.removeFirst())
78+
didRead = true
79+
}
80+
if didRead {
81+
context.fireChannelReadComplete()
82+
}
83+
context.leavePipeline(removalToken: removalToken)
84+
}
85+
86+
public func channelInactive(context: ChannelHandlerContext) {
87+
bufferedReads.removeAll()
88+
context.fireChannelInactive()
89+
}
90+
}

Sources/Containerization/Image/ImageStore/ImageStore+Export.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension ImageStore {
4040
}
4141

4242
@discardableResult
43-
public func export(index: Descriptor, platforms: (Platform) -> Bool) async throws -> Descriptor {
43+
public func export(index: Descriptor, platforms: (Platform) -> Bool, filter: (Descriptor) -> Bool = { _ in true }) async throws -> Descriptor {
4444
var pushQueue: [[Descriptor]] = []
4545
var current: [Descriptor] = [index]
4646
while !current.isEmpty {
@@ -61,7 +61,7 @@ extension ImageStore {
6161
try await withThrowingTaskGroup(of: Void.self) { group in
6262
for layerGroup in pushQueue.reversed() {
6363
for chunk in layerGroup.chunks(ofCount: 8) {
64-
for desc in chunk {
64+
for desc in chunk.filter(filter) {
6565
guard let content = try await self.contentStore.get(digest: desc.digest) else {
6666
throw ContainerizationError(.notFound, message: "content with digest \(desc.digest)")
6767
}

Sources/Containerization/LinuxContainer.swift

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,24 @@ public final class LinuxContainer: Container, Sendable {
246246
mutating func setErrored(error: Swift.Error) {
247247
self = .errored(error)
248248
}
249+
250+
func vm(_ operation: String) throws -> any VirtualMachineInstance {
251+
switch self {
252+
case .created(let state):
253+
return state.vm
254+
case .started(let state):
255+
return state.vm
256+
case .paused(let state):
257+
return state.vm
258+
case .errored(let err):
259+
throw err
260+
default:
261+
throw ContainerizationError(
262+
.invalidState,
263+
message: "failed to \(operation): container must be created, running, or paused"
264+
)
265+
}
266+
}
249267
}
250268

251269
private let vmm: VirtualMachineManager
@@ -590,12 +608,16 @@ extension LinuxContainer {
590608
// For every interface asked for:
591609
// 1. Add the address requested
592610
// 2. Online the adapter
593-
// 3. If a gateway IP address is present, add the default route.
611+
// 3. For the first interface, add the default route
612+
var defaultRouteSet = false
594613
for (index, i) in self.interfaces.enumerated() {
595614
let name = "eth\(index)"
596615
self.logger?.debug("setting up interface \(name) with address \(i.ipv4Address)")
597616
try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address)
598617
try await agent.up(name: name, mtu: i.mtu)
618+
if defaultRouteSet {
619+
continue
620+
}
599621
if let ipv4Gateway = i.ipv4Gateway {
600622
if !i.ipv4Address.contains(ipv4Gateway) {
601623
self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(i.ipv4Address), adding a route first")
@@ -606,6 +628,7 @@ extension LinuxContainer {
606628
self.logger?.debug("no gateway for \(name)")
607629
try await agent.routeAddDefault(name: name, ipv4Gateway: nil)
608630
}
631+
defaultRouteSet = true
609632
}
610633

611634
// Setup /etc/resolv.conf and /etc/hosts if asked for.
@@ -818,7 +841,7 @@ extension LinuxContainer {
818841
}
819842

820843
/// Send a signal to the container.
821-
public func kill(_ signal: Int32) async throws {
844+
public func kill(_ signal: Signal) async throws {
822845
try await self.state.withLock {
823846
let state = try $0.startedState("kill")
824847
try await state.process.kill(signal)
@@ -874,7 +897,7 @@ extension LinuxContainer {
874897
agent: agent,
875898
vm: startedState.vm,
876899
logger: self.logger,
877-
onDelete: { [weak self] in
900+
onDelete: { [weak self = self] in
878901
await self?.removeProcess(id: id)
879902
}
880903
)
@@ -911,7 +934,7 @@ extension LinuxContainer {
911934
agent: agent,
912935
vm: state.vm,
913936
logger: self.logger,
914-
onDelete: { [weak self] in
937+
onDelete: { [weak self = self] in
915938
await self?.removeProcess(id: id)
916939
}
917940
)
@@ -931,6 +954,20 @@ extension LinuxContainer {
931954
}
932955
}
933956

957+
/// Provides scoped access to the underlying virtual machine instance.
958+
///
959+
/// Most users should prefer the higher level APIs on ``LinuxContainer``
960+
/// directly. This is intended for advanced use cases that need to interact
961+
/// with the virtual machine outside of the container abstraction.
962+
public func withVirtualMachineInstance<T: Sendable>(
963+
_ fn: @Sendable (any VirtualMachineInstance) async throws -> T
964+
) async throws -> T {
965+
let vm = try await self.state.withLock { state in
966+
try state.vm("withVirtualMachineInstance")
967+
}
968+
return try await fn(vm)
969+
}
970+
934971
/// Close the containers standard input to signal no more input is
935972
/// arriving.
936973
public func closeStdin() async throws {

0 commit comments

Comments
 (0)