Skip to content

Commit f812e9e

Browse files
committed
Moving bundle creation to sandboxService
1 parent 3e49dce commit f812e9e

7 files changed

Lines changed: 199 additions & 23 deletions

File tree

Sources/ContainerResource/Container/Bundle.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,31 @@ import Containerization
1818
import ContainerizationError
1919
import Foundation
2020

21+
public struct BundleMetadata: Codable, Sendable {
22+
public let path: URL
23+
public let initialFilesystem: Filesystem
24+
public let kernel: Kernel
25+
public let containerConfiguration: ContainerConfiguration?
26+
public let containerRootFilesystem: Filesystem?
27+
public let options: ContainerCreateOptions?
28+
29+
public init(
30+
path: URL,
31+
initialFilesystem: Filesystem,
32+
kernel: Kernel,
33+
containerConfiguration: ContainerConfiguration? = nil,
34+
containerRootFilesystem: Filesystem? = nil,
35+
options: ContainerCreateOptions? = nil
36+
) {
37+
self.path = path
38+
self.initialFilesystem = initialFilesystem
39+
self.kernel = kernel
40+
self.containerConfiguration = containerConfiguration
41+
self.containerRootFilesystem = containerRootFilesystem
42+
self.options = options
43+
}
44+
}
45+
2146
public struct Bundle: Sendable {
2247
private static let initfsFilename = "initfs.ext4"
2348
private static let kernelFilename = "kernel.json"
@@ -109,6 +134,46 @@ extension Bundle {
109134
}
110135
return bundle
111136
}
137+
138+
public static func createFromMetadata(_ metadata: BundleMetadata) throws -> Bundle {
139+
let bundle = try create(
140+
path: metadata.path,
141+
initialFilesystem: metadata.initialFilesystem,
142+
kernel: metadata.kernel,
143+
containerConfiguration: metadata.containerConfiguration
144+
)
145+
146+
if let containerRootFs = metadata.containerRootFilesystem {
147+
let readonly = metadata.containerConfiguration?.readOnly ?? false
148+
try bundle.setContainerRootFs(cloning: containerRootFs, readonly: readonly)
149+
}
150+
151+
if let options = metadata.options {
152+
try bundle.write(filename: "options.json", value: options)
153+
}
154+
155+
return bundle
156+
}
157+
158+
public static func writeMetadata(
159+
_ metadata: BundleMetadata,
160+
to metadataPath: URL
161+
) throws {
162+
let data = try JSONEncoder().encode(metadata)
163+
try data.write(to: metadataPath)
164+
}
165+
166+
public static func readMetadata(from metadataPath: URL) throws -> BundleMetadata {
167+
guard FileManager.default.fileExists(atPath: metadataPath.path) else {
168+
throw ContainerizationError(
169+
.notFound,
170+
message: "Bundle metadata file not found at path: \(metadataPath.path)"
171+
)
172+
}
173+
174+
let data = try Data(contentsOf: metadataPath)
175+
return try JSONDecoder().decode(BundleMetadata.self, from: data)
176+
}
112177
}
113178

114179
extension Bundle {

Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper+Start.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ extension RuntimeLinuxHelper {
8989
connection: anonymousConnection,
9090
routes: [
9191
SandboxRoutes.bootstrap.rawValue: server.bootstrap,
92+
SandboxRoutes.createBundle.rawValue: server.createBundle,
9293
SandboxRoutes.createProcess.rawValue: server.createProcess,
9394
SandboxRoutes.state.rawValue: server.state,
9495
SandboxRoutes.stop.rawValue: server.stop,

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 92 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,23 @@ public actor ContainersService {
235235
let systemPlatform = kernel.platform
236236
let initFs = try await self.getInitBlock(for: systemPlatform.ociPlatform())
237237

238-
let bundle = try ContainerResource.Bundle.create(
239-
path: path,
240-
initialFilesystem: initFs,
241-
kernel: kernel,
242-
containerConfiguration: configuration
243-
)
238+
let metadataPath = FileManager.default.temporaryDirectory
239+
.appendingPathComponent("bundle-metadata-\(configuration.id).json")
240+
244241
do {
245242
let containerImage = ClientImage(description: configuration.image)
246243
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
247-
try bundle.setContainerRootFs(cloning: imageFs, readonly: configuration.readOnly)
248-
try bundle.write(filename: "options.json", value: options)
244+
245+
let completeMetadata = BundleMetadata(
246+
path: path,
247+
initialFilesystem: initFs,
248+
kernel: kernel,
249+
containerConfiguration: configuration,
250+
containerRootFilesystem: imageFs,
251+
options: options
252+
)
253+
254+
try ContainerResource.Bundle.writeMetadata(completeMetadata, to: metadataPath)
249255

250256
let snapshot = ContainerSnapshot(
251257
configuration: configuration,
@@ -255,11 +261,8 @@ public actor ContainersService {
255261
)
256262
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot), context: context)
257263
} catch {
258-
do {
259-
try bundle.delete()
260-
} catch {
261-
self.log.error("failed to delete bundle for container \(configuration.id): \(error)")
262-
}
264+
try? FileManager.default.removeItem(at: metadataPath)
265+
self.log.error("failed to cleanup metadata file for container \(configuration.id)")
263266
throw error
264267
}
265268
}
@@ -279,8 +282,7 @@ public actor ContainersService {
279282
}
280283

281284
let path = self.containerRoot.appendingPathComponent(id)
282-
let bundle = ContainerResource.Bundle(path: path)
283-
let config = try bundle.configuration
285+
let config = try await self.getContainerConfiguration(for: id, at: path)
284286

285287
do {
286288
try Self.registerService(
@@ -295,6 +297,13 @@ public actor ContainersService {
295297
id: id,
296298
runtime: runtime
297299
)
300+
301+
// Create bundle if it doesn't exist
302+
if !(await self.bundleExists(at: path)) {
303+
let metadataPath = Self.getMetadataPath(for: id)
304+
try await sandboxClient.createBundle(metadataPath: metadataPath)
305+
}
306+
298307
try await sandboxClient.bootstrap(stdio: stdio)
299308

300309
try await self.exitMonitor.registerProcess(
@@ -596,15 +605,39 @@ public actor ContainersService {
596605
// the OCI runtime.
597606
await self.exitMonitor.stopTracking(id: id)
598607
let path = self.containerRoot.appendingPathComponent(id)
608+
609+
// Try to get config for service deregistration, but don't fail if bundle is incomplete
610+
var config: ContainerConfiguration?
599611
let bundle = ContainerResource.Bundle(path: path)
600-
let config = try bundle.configuration
612+
do {
613+
config = try bundle.configuration
614+
} catch {
615+
self.log.warning("Unable to read bundle configuration during cleanup for container \(id): \(error)")
616+
}
617+
618+
// Only try to deregister service if we have a valid config
619+
if let config = config {
620+
let label = Self.fullLaunchdServiceLabel(
621+
runtimeName: config.runtimeHandler,
622+
instanceId: id
623+
)
624+
try? ServiceManager.deregister(fullServiceLabel: label)
625+
}
626+
627+
// Always try to delete the bundle directory, even if it's incomplete
628+
do {
629+
try bundle.delete()
630+
} catch {
631+
self.log.warning("Failed to delete bundle for container \(id): \(error)")
632+
// Still try to remove the directory manually if bundle.delete() fails
633+
try? FileManager.default.removeItem(at: path)
634+
}
635+
636+
// If a container is created and immediately removed (without starting),
637+
// the temp bundle metadata file never gets removed (so do that now)
638+
let metadataPath = Self.getMetadataPath(for: id)
639+
try? FileManager.default.removeItem(at: metadataPath)
601640

602-
let label = Self.fullLaunchdServiceLabel(
603-
runtimeName: config.runtimeHandler,
604-
instanceId: id
605-
)
606-
try ServiceManager.deregister(fullServiceLabel: label)
607-
try bundle.delete()
608641
self.containers.removeValue(forKey: id)
609642
}
610643

@@ -668,6 +701,43 @@ public actor ContainersService {
668701
private static func isInitProcess(id: String, processID: String) -> Bool {
669702
id == processID
670703
}
704+
705+
/// Check if a bundle exists at the given path
706+
private func bundleExists(at path: URL) async -> Bool {
707+
guard FileManager.default.fileExists(atPath: path.path) else {
708+
return false
709+
}
710+
711+
let bundle = ContainerResource.Bundle(path: path)
712+
do {
713+
_ = try bundle.configuration
714+
return true
715+
} catch {
716+
return false
717+
}
718+
}
719+
720+
/// Get metadata file path for the given container ID
721+
private static func getMetadataPath(for containerID: String) -> URL {
722+
FileManager.default.temporaryDirectory
723+
.appendingPathComponent("bundle-metadata-\(containerID).json")
724+
}
725+
726+
/// Get container configuration, either from existing bundle or from metadata
727+
private func getContainerConfiguration(for id: String, at path: URL) async throws -> ContainerConfiguration {
728+
guard await bundleExists(at: path) else {
729+
// Bundle doesn't exist, get config from metadata
730+
let metadataPath = Self.getMetadataPath(for: id)
731+
let metadata = try ContainerResource.Bundle.readMetadata(from: metadataPath)
732+
guard let config = metadata.containerConfiguration else {
733+
throw ContainerizationError(.internalError, message: "Metadata missing container configuration")
734+
}
735+
return config
736+
}
737+
// Bundle exists, read config normally
738+
let bundle = ContainerResource.Bundle(path: path)
739+
return try bundle.configuration
740+
}
671741
}
672742

673743
extension XPCMessage {

Sources/Services/ContainerSandboxService/Client/SandboxClient.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ extension SandboxClient {
106106
}
107107
}
108108

109+
public func createBundle(metadataPath: URL) async throws {
110+
let request = XPCMessage(route: SandboxRoutes.createBundle.rawValue)
111+
request.set(key: SandboxKeys.metadataPath.rawValue, value: metadataPath.path)
112+
do {
113+
_ = try await self.client.send(request)
114+
} catch {
115+
throw ContainerizationError(
116+
.internalError,
117+
message: "failed to create bundle for container \(self.id)",
118+
cause: error
119+
)
120+
}
121+
}
122+
109123
public func state() async throws -> SandboxSnapshot {
110124
let request = XPCMessage(route: SandboxRoutes.state.rawValue)
111125
let response: XPCMessage

Sources/Services/ContainerSandboxService/Client/SandboxKeys.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ public enum SandboxKeys: String {
4242

4343
/// Container statistics
4444
case statistics
45+
/// Path to bundle metadata file
46+
case metadataPath
4547
}

Sources/Services/ContainerSandboxService/Client/SandboxRoutes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,6 @@ public enum SandboxRoutes: String {
4141
case shutdown = "com.apple.container.sandbox/shutdown"
4242
/// Get statistics for the sandbox.
4343
case statistics = "com.apple.container.sandbox/statistics"
44+
/// Create bundle from metadata.
45+
case createBundle = "com.apple.container.sandbox/createBundle"
4446
}

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,29 @@ public actor SandboxService {
6060
return nil
6161
}
6262

63-
/// Create an instance with a bundle that describes the container.
63+
/// Create bundle from metadata file
64+
@Sendable
65+
public func createBundle(_ message: XPCMessage) async throws -> XPCMessage {
66+
self.log.info("`createBundle` xpc handler")
67+
68+
guard let metadataPathString = message.string(key: SandboxKeys.metadataPath.rawValue) else {
69+
throw ContainerizationError(.invalidArgument, message: "missing metadata path in createBundle xpc message")
70+
}
71+
72+
let metadataPath = URL(fileURLWithPath: metadataPathString)
73+
74+
do {
75+
let metadata = try ContainerResource.Bundle.readMetadata(from: metadataPath)
76+
_ = try ContainerResource.Bundle.createFromMetadata(metadata)
77+
try FileManager.default.removeItem(at: metadataPath)
78+
self.log.info("Created bundle from metadata at \(metadata.path)")
79+
} catch {
80+
try? FileManager.default.removeItem(at: metadataPath)
81+
throw error
82+
}
83+
84+
return message.reply()
85+
}
6486
///
6587
/// - Parameters:
6688
/// - root: The file URL for the bundle root.

0 commit comments

Comments
 (0)