From 421f3fc37301e1e9cb2de7e5cc3de8ab78b96fc7 Mon Sep 17 00:00:00 2001 From: Chris George Date: Sat, 9 May 2026 09:20:41 -0700 Subject: [PATCH] Use FilePath for ContainerResource.Bundle. Closes CHAOS-1460. Unblocks CHAOS-1467 (server-side catch-all). --- .../ContainerResource/Container/Bundle.swift | 66 ++++++++++--------- .../Server/Containers/ContainersService.swift | 29 ++++---- .../Runtime/RuntimeClient/Bundle+Log.swift | 5 +- .../RuntimeLinux/Server/RuntimeService.swift | 12 ++-- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/Sources/ContainerResource/Container/Bundle.swift b/Sources/ContainerResource/Container/Bundle.swift index 217531b8c..6f54407f2 100644 --- a/Sources/ContainerResource/Container/Bundle.swift +++ b/Sources/ContainerResource/Container/Bundle.swift @@ -17,6 +17,7 @@ import Containerization import ContainerizationError import Foundation +import SystemPackage public struct Bundle: Sendable { private static let initfsFilename = "initfs.ext4" @@ -28,27 +29,28 @@ public struct Bundle: Sendable { static let containerConfigFilename = "config.json" /// The path to the bundle. - public let path: URL + public let path: FilePath - public init(path: URL) { + public init(path: FilePath) { self.path = path } - public var bootlog: URL { - self.path.appendingPathComponent("vminitd.log") + public var bootlog: FilePath { + self.path.appending("vminitd.log") } - public var containerRootfsBlock: URL { - self.path.appendingPathComponent(Self.containerRootFsBlockFilename) + public var containerRootfsBlock: FilePath { + self.path.appending(Self.containerRootFsBlockFilename) } - private var containerRootfsConfig: URL { - self.path.appendingPathComponent(Self.containerRootFsFilename) + private var containerRootfsConfig: FilePath { + self.path.appending(Self.containerRootFsFilename) } public var containerRootfs: Filesystem { get throws { - let data = try Data(contentsOf: containerRootfsConfig) + // Foundation's `Data(contentsOf:)` only accepts `URL`, so bridge here. + let data = try Data(contentsOf: URL(filePath: containerRootfsConfig.string)) let fs = try JSONDecoder().decode(Filesystem.self, from: data) return fs } @@ -58,7 +60,7 @@ public struct Bundle: Sendable { public var initialFilesystem: Filesystem { .block( format: "ext4", - source: self.path.appendingPathComponent(Self.initfsFilename).path, + source: self.path.appending(Self.initfsFilename).string, destination: "/", options: ["ro"] ) @@ -66,32 +68,33 @@ public struct Bundle: Sendable { public var kernel: Kernel { get throws { - try load(path: self.path.appendingPathComponent(Self.kernelFilename)) + try load(path: self.path.appending(Self.kernelFilename)) } } public var configuration: ContainerConfiguration { get throws { - try load(path: self.path.appendingPathComponent(Self.containerConfigFilename)) + try load(path: self.path.appending(Self.containerConfigFilename)) } } } extension Bundle { public static func create( - path: URL, + path: FilePath, initialFilesystem: Filesystem, kernel: Kernel, containerConfiguration: ContainerConfiguration? = nil, containerRootFilesystem: Filesystem? = nil, options: ContainerCreateOptions? = nil ) throws -> Bundle { - try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) - let kbin = path.appendingPathComponent(Self.kernelBinaryFilename) - try FileManager.default.copyItem(at: kernel.path, to: kbin) + try FileManager.default.createDirectory(atPath: path.string, withIntermediateDirectories: true) + let kbin = path.appending(Self.kernelBinaryFilename) + // `Kernel.path` is `URL` (Containerization API), so bridge across the FilePath/URL boundary. + try FileManager.default.copyItem(at: kernel.path, to: URL(filePath: kbin.string)) var k = kernel - k.path = kbin - try write(path.appendingPathComponent(Self.kernelFilename), value: k) + k.path = URL(filePath: kbin.string) + try write(path.appending(Self.kernelFilename), value: k) switch initialFilesystem.type { case .block(let fmt, _, _): @@ -101,7 +104,7 @@ extension Bundle { // when saving the Initial Filesystem to the bundle // discard any filesystem information and just persist // the block into the Bundle. - _ = try initialFilesystem.clone(to: path.appendingPathComponent(Self.initfsFilename).path) + _ = try initialFilesystem.clone(to: path.appending(Self.initfsFilename).string) default: fatalError("invalid filesystem type for initial filesystem") } @@ -131,13 +134,14 @@ extension Bundle { } /// Return the full filepath for a named resource in the Bundle. - public func filePath(for name: String) -> URL { - path.appendingPathComponent(name) + public func filePath(for name: String) -> FilePath { + path.appending(name) } public func setContainerRootFs(fs: Filesystem) throws { let fsData = try JSONEncoder().encode(fs) - try fsData.write(to: self.containerRootfsConfig) + // Foundation's `Data.write(to:)` only accepts `URL`, so bridge here. + try fsData.write(to: URL(filePath: self.containerRootfsConfig.string)) } public func cloneContainerRootFs(cloning fs: Filesystem, readonly: Bool = false) throws { @@ -145,30 +149,32 @@ extension Bundle { if readonly && !mutableFs.options.contains("ro") { mutableFs.options.append("ro") } - let cloned = try mutableFs.clone(to: self.containerRootfsBlock.absolutePath()) + let cloned = try mutableFs.clone(to: self.containerRootfsBlock.string) try setContainerRootFs(fs: cloned) } /// Delete the bundle and all of the resources contained inside. public func delete() throws { - try FileManager.default.removeItem(at: self.path) + try FileManager.default.removeItem(atPath: self.path.string) } public func write(filename: String, value: Encodable) throws { - try Self.write(self.path.appendingPathComponent(filename), value: value) + try Self.write(self.path.appending(filename), value: value) } - private static func write(_ path: URL, value: Encodable) throws { + private static func write(_ path: FilePath, value: Encodable) throws { let data = try JSONEncoder().encode(value) - try data.write(to: path) + // Foundation's `Data.write(to:)` only accepts `URL`, so bridge here. + try data.write(to: URL(filePath: path.string)) } public func load(filename: String) throws -> T where T: Decodable { - try load(path: self.path.appendingPathComponent(filename)) + try load(path: self.path.appending(filename)) } - private func load(path: URL) throws -> T where T: Decodable { - let data = try Data(contentsOf: path) + private func load(path: FilePath) throws -> T where T: Decodable { + // Foundation's `Data(contentsOf:)` only accepts `URL`, so bridge here. + let data = try Data(contentsOf: URL(filePath: path.string)) return try JSONDecoder().decode(T.self, from: data) } } diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift index 81612495f..c2f613c8b 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift @@ -127,7 +127,7 @@ public actor ContainersService { ) } - let bundle = ContainerResource.Bundle(path: dir) + let bundle = ContainerResource.Bundle(path: FilePath(dir.path)) try? bundle.delete() continue } @@ -749,10 +749,10 @@ public actor ContainersService { do { _ = try _getContainerState(id: id) let path = self.containerRoot.appendingPathComponent(id) - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) return [ - try FileHandle(forReadingFrom: bundle.containerLog), - try FileHandle(forReadingFrom: bundle.bootlog), + try FileHandle(forReadingFrom: URL(filePath: bundle.containerLog.string)), + try FileHandle(forReadingFrom: URL(filePath: bundle.bootlog.string)), ] } catch { throw ContainerizationError( @@ -902,18 +902,17 @@ public actor ContainersService { let state = try self._getContainerState(id: id) let path = self.containerRoot.appendingPathComponent(id) - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) let rootfs = bundle.containerRootfsBlock - switch state.snapshot.status { case .running: let client = try state.getClient() - let snapshot = rootfs.appendingPathExtension("snapshot") - defer { try? FileManager.default.removeItem(at: snapshot) } - try await client.snapshotDisk(imagePath: rootfs.path, destinationPath: snapshot.path) - try EXT4.EXT4Reader(blockDevice: FilePath(snapshot)).export(archive: FilePath(archive)) + let snapshot = rootfs.appending(".snapshot") + defer { try? FileManager.default.removeItem(atPath: snapshot.string) } + try await client.snapshotDisk(imagePath: rootfs.string, destinationPath: snapshot.string) + try EXT4.EXT4Reader(blockDevice: snapshot).export(archive: FilePath(archive)) case .stopped: - try EXT4.EXT4Reader(blockDevice: FilePath(rootfs)).export(archive: FilePath(archive)) + try EXT4.EXT4Reader(blockDevice: rootfs).export(archive: FilePath(archive)) default: throw ContainerizationError(.invalidState, message: "container must be running or stopped") } @@ -952,7 +951,7 @@ public actor ContainersService { self.log.info("shutting down runtime service", metadata: ["id": "\(id)"]) let path = self.containerRoot.appendingPathComponent(id) - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) let config = try bundle.configuration let label = Self.fullLaunchdServiceLabel( runtimeName: config.runtimeHandler, @@ -1037,7 +1036,7 @@ public actor ContainersService { // Try to get config for service deregistration // Don't fail if bundle is incomplete var config: ContainerConfiguration? - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) do { config = try bundle.configuration } catch { @@ -1081,7 +1080,7 @@ public actor ContainersService { private func getContainerCreationOptions(id: String) throws -> ContainerCreateOptions { let path = self.containerRoot.appendingPathComponent(id) - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) let options: ContainerCreateOptions = try bundle.load(filename: "options.json") return options } @@ -1140,7 +1139,7 @@ public actor ContainersService { /// Get container configuration, either from existing bundle or from RuntimeConfiguration private static func getContainerConfiguration(at path: URL) throws -> (ContainerConfiguration, ContainerCreateOptions?) { - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) do { let config = try bundle.configuration let options: ContainerCreateOptions? = try? bundle.load(filename: "options.json") diff --git a/Sources/Services/Runtime/RuntimeClient/Bundle+Log.swift b/Sources/Services/Runtime/RuntimeClient/Bundle+Log.swift index 72cc27d84..ef586dacb 100644 --- a/Sources/Services/Runtime/RuntimeClient/Bundle+Log.swift +++ b/Sources/Services/Runtime/RuntimeClient/Bundle+Log.swift @@ -16,10 +16,11 @@ import ContainerResource import Foundation +import SystemPackage extension ContainerResource.Bundle { /// The pathname for the workload log file. - public var containerLog: URL { - path.appendingPathComponent("stdio.log") + public var containerLog: FilePath { + path.appending("stdio.log") } } diff --git a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift index 948a65603..3f0471951 100644 --- a/Sources/Services/RuntimeLinux/Server/RuntimeService.swift +++ b/Sources/Services/RuntimeLinux/Server/RuntimeService.swift @@ -154,7 +154,7 @@ public actor RuntimeService { let dynamicEnv = try message.dynamicEnv() - let bundle = ContainerResource.Bundle(path: self.root) + let bundle = ContainerResource.Bundle(path: FilePath(self.root.path)) try bundle.createLogFile() var config = try bundle.configuration @@ -239,7 +239,7 @@ public actor RuntimeService { } let stdio = message.stdio() - let containerLog = try FileHandle(forWritingTo: bundle.containerLog) + let containerLog = try FileHandle(forWritingTo: URL(filePath: bundle.containerLog.string)) let stdout = { if let h = stdio[1] { return MultiWriter(handles: [h, containerLog]) @@ -281,7 +281,7 @@ public actor RuntimeService { )) } czConfig.hosts = Hosts(entries: hostsEntries) - czConfig.bootLog = BootLog.file(path: bundle.bootlog, append: true) + czConfig.bootLog = BootLog.file(path: URL(filePath: bundle.bootlog.string), append: true) } let ctrInfo = ContainerInfo( @@ -1417,7 +1417,7 @@ extension ContainerResource.Bundle { func createLogFile() throws { // Create the log file we'll write stdio to. // O_TRUNC resolves a log delay issue on restarted containers by force-updating internal state - let fd = Darwin.open(self.containerLog.path, O_CREAT | O_RDONLY | O_TRUNC, 0o644) + let fd = Darwin.open(self.containerLog.string, O_CREAT | O_RDONLY | O_TRUNC, 0o644) guard fd > 0 else { throw POSIXError(.init(rawValue: errno)!) } @@ -1636,7 +1636,7 @@ extension RuntimeService { return false } - let bundle = ContainerResource.Bundle(path: path) + let bundle = ContainerResource.Bundle(path: FilePath(path.path)) do { _ = try bundle.configuration return true @@ -1650,7 +1650,7 @@ extension RuntimeService { do { let runtimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: self.root) _ = try ContainerResource.Bundle.create( - path: runtimeConfig.path, + path: FilePath(runtimeConfig.path.path), initialFilesystem: runtimeConfig.initialFilesystem, kernel: runtimeConfig.kernel, containerConfiguration: runtimeConfig.containerConfiguration,