Skip to content

Commit cfc9953

Browse files
committed
# This is a combination of 3 commits.
# This is the 1st commit message: Moving bundle creation to sandboxService # This is the commit message #2: make fmt # This is the commit message #3: removing stray comment
1 parent cf9b335 commit cfc9953

6 files changed

Lines changed: 276 additions & 30 deletions

File tree

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ let package = Package(
338338
dependencies: [
339339
.product(name: "Containerization", package: "containerization"),
340340
.product(name: "ContainerizationExtras", package: "containerization"),
341+
"ContainerAPIService",
341342
"ContainerResource",
342343
]
343344
),

Sources/ContainerResource/Container/Bundle.swift

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

21+
public struct BundleMetadata: Codable, Sendable {
22+
static let bundleMetadataFileName = "bundle-metadata.json"
23+
24+
public let path: URL
25+
public let initialFilesystem: Filesystem
26+
public let kernel: Kernel
27+
public let containerConfiguration: ContainerConfiguration?
28+
public let containerRootFilesystem: Filesystem?
29+
public let options: ContainerCreateOptions?
30+
31+
public init(
32+
path: URL,
33+
initialFilesystem: Filesystem,
34+
kernel: Kernel,
35+
containerConfiguration: ContainerConfiguration? = nil,
36+
containerRootFilesystem: Filesystem? = nil,
37+
options: ContainerCreateOptions? = nil
38+
) {
39+
self.path = path
40+
self.initialFilesystem = initialFilesystem
41+
self.kernel = kernel
42+
self.containerConfiguration = containerConfiguration
43+
self.containerRootFilesystem = containerRootFilesystem
44+
self.options = options
45+
}
46+
47+
public var bundleMetadataPath: URL {
48+
self.path.appendingPathComponent(Self.bundleMetadataFileName)
49+
}
50+
51+
public static func writeMetadata(_ metadata: BundleMetadata) throws {
52+
// Ensure the parent directory exists
53+
let directory = metadata.bundleMetadataPath.deletingLastPathComponent()
54+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
55+
56+
let data = try JSONEncoder().encode(metadata)
57+
try data.write(to: metadata.bundleMetadataPath)
58+
}
59+
60+
public static func readMetadata(from bundlePath: URL) throws -> BundleMetadata {
61+
let metadataPath = bundlePath.appendingPathComponent(BundleMetadata.bundleMetadataFileName)
62+
guard FileManager.default.fileExists(atPath: metadataPath.path) else {
63+
throw ContainerizationError(
64+
.notFound,
65+
message: "bundle metadata file not found at path: \(metadataPath.path)"
66+
)
67+
}
68+
69+
let data = try Data(contentsOf: metadataPath)
70+
return try JSONDecoder().decode(BundleMetadata.self, from: data)
71+
}
72+
}
73+
2174
public struct Bundle: Sendable {
2275
private static let initfsFilename = "initfs.ext4"
2376
private static let kernelFilename = "kernel.json"
@@ -109,6 +162,26 @@ extension Bundle {
109162
}
110163
return bundle
111164
}
165+
166+
public static func createFromMetadata(_ metadata: BundleMetadata) throws -> Bundle {
167+
let bundle = try create(
168+
path: metadata.path,
169+
initialFilesystem: metadata.initialFilesystem,
170+
kernel: metadata.kernel,
171+
containerConfiguration: metadata.containerConfiguration
172+
)
173+
174+
if let containerRootFs = metadata.containerRootFilesystem {
175+
let readonly = metadata.containerConfiguration?.readOnly ?? false
176+
try bundle.setContainerRootFs(cloning: containerRootFs, readonly: readonly)
177+
}
178+
179+
if let options = metadata.options {
180+
try bundle.write(filename: "options.json", value: options)
181+
}
182+
183+
return bundle
184+
}
112185
}
113186

114187
extension Bundle {

Sources/Helpers/RuntimeLinux/RuntimeLinuxHelper+Start.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ extension RuntimeLinuxHelper {
6969
}
7070

7171
nonisolated(unsafe) let anonymousConnection = xpc_connection_create(nil, nil)
72+
7273
let server = SandboxService(
7374
root: .init(fileURLWithPath: root),
7475
interfaceStrategy: interfaceStrategy,

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

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -238,17 +238,20 @@ public actor ContainersService {
238238
self.log.info("Using init image: \(initImage ?? ClientImage.initImageRef)")
239239
let initFilesystem = try await self.getInitBlock(for: systemPlatform.ociPlatform(), imageRef: initImage)
240240

241-
let bundle = try ContainerResource.Bundle.create(
242-
path: path,
243-
initialFilesystem: initFilesystem,
244-
kernel: kernel,
245-
containerConfiguration: configuration
246-
)
247241
do {
248242
let containerImage = ClientImage(description: configuration.image)
249243
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
250-
try bundle.setContainerRootFs(cloning: imageFs, readonly: configuration.readOnly)
251-
try bundle.write(filename: "options.json", value: options)
244+
245+
let completeMetadata = BundleMetadata(
246+
path: path,
247+
initialFilesystem: initFilesystem,
248+
kernel: kernel,
249+
containerConfiguration: configuration,
250+
containerRootFilesystem: imageFs,
251+
options: options
252+
)
253+
254+
try ContainerResource.BundleMetadata.writeMetadata(completeMetadata)
252255

253256
let snapshot = ContainerSnapshot(
254257
configuration: configuration,
@@ -258,11 +261,6 @@ public actor ContainersService {
258261
)
259262
await self.setContainerState(configuration.id, ContainerState(snapshot: snapshot), context: context)
260263
} catch {
261-
do {
262-
try bundle.delete()
263-
} catch {
264-
self.log.error("failed to delete bundle for container \(configuration.id): \(error)")
265-
}
266264
throw error
267265
}
268266
}
@@ -282,8 +280,7 @@ public actor ContainersService {
282280
}
283281

284282
let path = self.containerRoot.appendingPathComponent(id)
285-
let bundle = ContainerResource.Bundle(path: path)
286-
let config = try bundle.configuration
283+
let config = try await self.getContainerConfiguration(at: path)
287284

288285
do {
289286
try Self.registerService(
@@ -298,6 +295,7 @@ public actor ContainersService {
298295
id: id,
299296
runtime: runtime
300297
)
298+
301299
try await sandboxClient.bootstrap(stdio: stdio)
302300

303301
try await self.exitMonitor.registerProcess(
@@ -602,15 +600,33 @@ public actor ContainersService {
602600
// the OCI runtime.
603601
await self.exitMonitor.stopTracking(id: id)
604602
let path = self.containerRoot.appendingPathComponent(id)
603+
604+
// Try to get config for service deregistration
605+
// Don't fail if bundle is incomplete
606+
var config: ContainerConfiguration?
605607
let bundle = ContainerResource.Bundle(path: path)
606-
let config = try bundle.configuration
608+
do {
609+
config = try bundle.configuration
610+
} catch {
611+
self.log.warning("Unable to read bundle configuration during cleanup for container \(id): \(error)")
612+
}
613+
614+
// Only try to deregister service if we have a valid config
615+
if let config = config {
616+
let label = Self.fullLaunchdServiceLabel(
617+
runtimeName: config.runtimeHandler,
618+
instanceId: id
619+
)
620+
try? ServiceManager.deregister(fullServiceLabel: label)
621+
}
622+
623+
// Always try to delete the bundle directory, even if it's incomplete
624+
do {
625+
try bundle.delete()
626+
} catch {
627+
self.log.warning("Failed to delete bundle for container \(id): \(error)")
628+
}
607629

608-
let label = Self.fullLaunchdServiceLabel(
609-
runtimeName: config.runtimeHandler,
610-
instanceId: id
611-
)
612-
try ServiceManager.deregister(fullServiceLabel: label)
613-
try bundle.delete()
614630
self.containers.removeValue(forKey: id)
615631
}
616632

@@ -675,6 +691,35 @@ public actor ContainersService {
675691
private static func isInitProcess(id: String, processID: String) -> Bool {
676692
id == processID
677693
}
694+
695+
/// Check if a bundle exists at the given path
696+
private func bundleExists(at path: URL) async -> ContainerResource.ContainerConfiguration? {
697+
guard FileManager.default.fileExists(atPath: path.path) else {
698+
return nil
699+
}
700+
701+
let bundle = ContainerResource.Bundle(path: path)
702+
do {
703+
let config = try bundle.configuration
704+
return config
705+
} catch {
706+
return nil
707+
}
708+
}
709+
710+
/// Get container configuration, either from existing bundle or from metadata
711+
private func getContainerConfiguration(at path: URL) async throws -> ContainerConfiguration {
712+
guard let config = await bundleExists(at: path) else {
713+
// Bundle doesn't exist, get config from metadata
714+
let metadata = try ContainerResource.BundleMetadata.readMetadata(from: path)
715+
guard let config = metadata.containerConfiguration else {
716+
throw ContainerizationError(.internalError, message: "metadata missing container configuration")
717+
}
718+
return config
719+
}
720+
// Bundle exists, read config normally
721+
return config
722+
}
678723
}
679724

680725
extension XPCMessage {

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,6 @@ public actor SandboxService {
6060
return nil
6161
}
6262

63-
/// Create an instance with a bundle that describes the container.
64-
///
65-
/// - Parameters:
66-
/// - root: The file URL for the bundle root.
67-
/// - interfaceStrategy: The strategy for producing network interface
68-
/// objects for each network to which the container attaches.
69-
/// - log: The destination for log messages.
7063
public init(
7164
root: URL,
7265
interfaceStrategy: InterfaceStrategy,
@@ -108,6 +101,12 @@ public actor SandboxService {
108101
@Sendable
109102
public func bootstrap(_ message: XPCMessage) async throws -> XPCMessage {
110103
self.log.info("`bootstrap` xpc handler")
104+
105+
// Create the bundle if it doesn't exist yet
106+
if !self.bundleExists(at: self.root) {
107+
try self.createBundle()
108+
}
109+
111110
return try await self.lock.withLock { _ in
112111
guard await self.state == .created else {
113112
throw ContainerizationError(
@@ -1225,7 +1224,7 @@ extension FileHandle: @retroactive ReaderStream, @retroactive Writer {
12251224
}
12261225
}
12271226

1228-
// MARK: State handler helpers
1227+
// MARK: State handler and bundle creation helpers
12291228

12301229
extension SandboxService {
12311230
private func addWaiter(id: String, cont: CheckedContinuation<ExitStatus, Never>) {
@@ -1300,4 +1299,33 @@ extension SandboxService {
13001299
func setState(_ new: State) {
13011300
self.state = new
13021301
}
1302+
1303+
/// Check if a bundle exists at the given path
1304+
private func bundleExists(at path: URL) -> Bool {
1305+
guard FileManager.default.fileExists(atPath: path.path) else {
1306+
return false
1307+
}
1308+
1309+
let bundle = ContainerResource.Bundle(path: path)
1310+
do {
1311+
_ = try bundle.configuration
1312+
return true
1313+
} catch {
1314+
return false
1315+
}
1316+
}
1317+
1318+
/// Create bundle from metadata
1319+
private func createBundle() throws {
1320+
do {
1321+
let metadata = try ContainerResource.BundleMetadata.readMetadata(from: self.root)
1322+
_ = try ContainerResource.Bundle.createFromMetadata(metadata)
1323+
self.log.info("Created bundle from metadata at \(metadata.path)")
1324+
// Could remove the metadata file at this point, but will be
1325+
// cleaned up along with the bundle anyway
1326+
} catch {
1327+
self.log.error("Failed to create bundle \(error)")
1328+
throw error
1329+
}
1330+
}
13031331
}

0 commit comments

Comments
 (0)