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
66 changes: 36 additions & 30 deletions Sources/ContainerResource/Container/Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import Containerization
import ContainerizationError
import Foundation
import SystemPackage

public struct Bundle: Sendable {
private static let initfsFilename = "initfs.ext4"
Expand All @@ -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
}
Expand All @@ -58,40 +60,41 @@ 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"]
)
}

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, _, _):
Expand All @@ -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")
}
Expand Down Expand Up @@ -131,44 +134,47 @@ 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 {
var mutableFs = fs
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<T>(filename: String) throws -> T where T: Decodable {
try load(path: self.path.appendingPathComponent(filename))
try load(path: self.path.appending(filename))
}

private func load<T>(path: URL) throws -> T where T: Decodable {
let data = try Data(contentsOf: path)
private func load<T>(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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 3 additions & 2 deletions Sources/Services/Runtime/RuntimeClient/Bundle+Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
12 changes: 6 additions & 6 deletions Sources/Services/RuntimeLinux/Server/RuntimeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)!)
}
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down