Add OCI transport and base clone with DiskImageKit (#1304)

* Add OCI transport and base clone with DiskImageKit

* Address stacked OCI pull review feedback

* Stream file digest hashing

* Lock frozen overlays during push
This commit is contained in:
Yibo Zhuang
2026-08-12 12:10:28 -07:00
committed by GitHub
parent f87b57bbc5
commit 4ce8a115f7
27 changed files with 2054 additions and 213 deletions
+49 -8
View File
@@ -31,6 +31,9 @@ struct Clone: AsyncParsableCommand {
@Flag(help: .hidden)
var deduplicate: Bool = false
@Flag(help: "create a stacked disk that uses the source image as an immutable base")
var stacked: Bool = false
@Option(help: ArgumentHelp("limit automatic pruning to n gigabytes", valueName: "n"))
var pruneLimit: UInt = 100
@@ -47,8 +50,15 @@ struct Clone: AsyncParsableCommand {
func run() async throws {
let ociStorage = try VMStorageOCI()
let localStorage = try VMStorageLocal()
let remoteName = try? RemoteName(sourceName)
if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) {
if stacked {
guard remoteName != nil else {
throw ValidationError("--stacked requires a remote image")
}
}
if let remoteName, try !ociStorage.hasUsableCachedImageForClone(remoteName, requireManifest: stacked) {
// Pull the VM in case it's OCI-based and doesn't exist locally yet
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure)
try await ociStorage.pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate)
@@ -66,9 +76,28 @@ struct Clone: AsyncParsableCommand {
let lock = try FileLock(lockURL: Config().tartHomeDir)
try lock.lock()
let sourceState = try sourceVM.state()
let generateMAC = try localStorage.hasVMsWithMACAddress(macAddress: sourceVM.macAddress())
&& sourceVM.state() != .Suspended
try sourceVM.clone(to: tmpVMDir, generateMAC: generateMAC)
&& sourceState != .Suspended
if stacked {
guard sourceVM.isStandalone else {
throw ValidationError("--stacked cannot use an image that already has a stacked disk")
}
guard try VMConfig(fromURL: sourceVM.configURL).os == .darwin else {
throw ValidationError("--stacked currently supports only macOS images")
}
try sourceVM.cloneAsStackedBase(to: tmpVMDir, generateMAC: generateMAC)
} else if sourceVM.isStackedCachedImage {
try sourceVM.cloneStacked(to: tmpVMDir, copyWritableOverlay: false, generateMAC: generateMAC)
} else if sourceVM.isStackedVM {
guard sourceState == .Stopped else {
throw RuntimeError.VMConfigurationError("VM \"\(sourceName)\" must be stopped before cloning")
}
try sourceVM.cloneStacked(to: tmpVMDir, copyWritableOverlay: true, generateMAC: generateMAC)
} else {
try sourceVM.clone(to: tmpVMDir, generateMAC: generateMAC)
}
try localStorage.move(newName, from: tmpVMDir)
@@ -78,11 +107,23 @@ struct Clone: AsyncParsableCommand {
// is not actually claiming new space until the VM is started and it writes something to disk.
//
// So, once we clone the VM let's try to claim the rest of space for the VM to run without errors.
let unallocatedBytes = try sourceVM.sizeBytes() - sourceVM.allocatedSizeBytes()
// Avoid reclaiming an excessive amount of disk space.
let reclaimBytes = min(unallocatedBytes, Int(pruneLimit) * 1024 * 1024 * 1024)
if reclaimBytes > 0 {
try Prune.reclaimIfNeeded(UInt64(reclaimBytes), sourceVM)
if sourceVM.isStandalone {
let unallocatedBytes = try sourceVM.sizeBytes() - sourceVM.allocatedSizeBytes()
// Avoid reclaiming an excessive amount of disk space.
let reclaimBytes = min(unallocatedBytes, Int(pruneLimit) * 1024 * 1024 * 1024)
if reclaimBytes > 0 {
try Prune.reclaimIfNeeded(UInt64(reclaimBytes), sourceVM)
}
} else if sourceVM.isStackedVM || sourceVM.isStackedCachedImage {
let clonedVM = try localStorage.open(newName)
// A stacked clone owns only its writable overlay locally, but that
// overlay may grow to the full guest-visible disk block layout at
// runtime. Reclaim against the clone so it is not pruned itself.
let unallocatedBytes = try clonedVM.diskSizeBytes() - clonedVM.allocatedSizeBytes()
let reclaimBytes = min(unallocatedBytes, Int(pruneLimit) * 1024 * 1024 * 1024)
if reclaimBytes > 0 {
try Prune.reclaimIfNeeded(UInt64(reclaimBytes), clonedVM)
}
}
}, onCancel: {
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
+1 -2
View File
@@ -31,7 +31,7 @@ struct Get: AsyncParsableCommand {
OS: vmConfig.os,
CPU: vmConfig.cpuCount,
Memory: memorySizeInMb,
Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 },
Disk: HumanReadableByteCount(try vmDir.diskSizeBytes()) { $0 / 1000 / 1000 / 1000 },
DiskFormat: vmConfig.diskFormat.rawValue,
Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) {
String(format: "%.3f", Float($0) / 1000 / 1000 / 1000)
@@ -40,7 +40,6 @@ struct Get: AsyncParsableCommand {
Running: try vmDir.running(),
State: try vmDir.state().rawValue
)
print(format.renderSingle(info))
}
}
+5
View File
@@ -31,6 +31,11 @@ struct Import: AsyncParsableCommand {
print("importing...")
try tmpVMDir.importFromArchive(path: path)
if tmpVMDir.isStackedVM || tmpVMDir.isStackedCachedImage {
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
throw RuntimeError.ImportFailed("importing stacked VMs is not supported yet")
}
try await withTaskCancellationHandler(operation: {
// Acquire a global lock
let lock = try FileLock(lockURL: Config().tartHomeDir)
+2 -2
View File
@@ -42,7 +42,7 @@ struct List: AsyncParsableCommand {
try VMInfo(
Source: "local",
Name: name,
Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 },
Disk: HumanReadableByteCount(try vmDir.diskSizeBytes()) { $0 / 1000 / 1000 / 1000 },
Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) { $0 / 1000 / 1000 / 1000 },
Accessed: formatAccessDate(try vmDir.accessDate()),
Running: vmDir.running(),
@@ -56,7 +56,7 @@ struct List: AsyncParsableCommand {
try VMInfo(
Source: "OCI",
Name: name,
Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 },
Disk: HumanReadableByteCount(try vmDir.diskSizeBytes()) { $0 / 1000 / 1000 / 1000 },
Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) { $0 / 1000 / 1000 / 1000 },
Accessed: formatAccessDate(try vmDir.accessDate()),
Running: vmDir.running(),
+6 -5
View File
@@ -69,7 +69,7 @@ struct Push: AsyncParsableCommand {
let references = remoteNamesForRegistry.map{ $0.reference.value }
let pushedRemoteName: RemoteName
// If we're pushing a local OCI VM, check if points to an already existing registry manifest
// If we're pushing a cached remote image, check if it points to an existing registry manifest
// and if so, only upload manifests (without config, disk and NVRAM) to the user-specified references
if let remoteName = try? RemoteName(localName) {
pushedRemoteName = try await lightweightPushToRegistry(
@@ -78,17 +78,18 @@ struct Push: AsyncParsableCommand {
references: references
)
} else {
pushedRemoteName = try await localVMDir.pushToRegistry(
let pushedImage = try await localVMDir.pushToRegistry(
registry: registry,
references: references,
chunkSizeMb: chunkSize,
concurrency: concurrency,
labels: parseLabels()
)
pushedRemoteName = pushedImage.name
// Populate the local cache (if requested)
if populateCache {
let expectedPushedVMDir = try ociStorage.create(pushedRemoteName)
try localVMDir.clone(to: expectedPushedVMDir, generateMAC: false)
try ociStorage.populate(pushedImage.name, from: localVMDir, manifest: pushedImage.manifest)
}
}
@@ -102,7 +103,7 @@ struct Push: AsyncParsableCommand {
}
func lightweightPushToRegistry(registry: Registry, remoteName: RemoteName, references: [String]) async throws -> RemoteName {
// Is the local OCI VM already present in the registry?
// Is the cached remote image already present in the registry?
let digest = try VMStorageOCI().digest(remoteName)
let (remoteManifest, _) = try await registry.pullManifest(reference: digest)
+52 -9
View File
@@ -455,10 +455,15 @@ struct Run: AsyncParsableCommand {
let provisioning = try provisioningOpts.map { try GuestProvisioningOptions($0) }
#endif
// Keep these values alive while the VM runs. Some additional disks own a
// lock that protects their temporary backing files from Config.gc().
let additionalDisks = try additionalDisks()
defer { withExtendedLifetime(additionalDisks) {} }
vm = try VM(
vmDir: vmDir,
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
additionalStorageDevices: try additionalDiskAttachments(),
additionalStorageDevices: additionalDisks.map(\.configuration),
directorySharingDevices: directoryShares() + rosettaDirectoryShare(),
serialPorts: serialPorts,
suspendable: suspendable,
@@ -727,9 +732,9 @@ struct Run: AsyncParsableCommand {
}
}
func additionalDiskAttachments() throws -> [VZStorageDeviceConfiguration] {
func additionalDisks() throws -> [AdditionalDisk] {
try disk.map {
try AdditionalDisk(parseFrom: $0).configuration
try AdditionalDisk(parseFrom: $0)
}
}
@@ -952,14 +957,32 @@ struct VMView: NSViewRepresentable {
struct AdditionalDisk {
let configuration: VZStorageDeviceConfiguration
// Retained for as long as the additional disk is attached, so Config.gc()
// cannot remove a temporary backing file or stacked-disk directory.
private let temporaryDiskLock: FileLock?
init(parseFrom: String) throws {
let (diskPath, readOnly, syncModeRaw, cachingModeRaw) = Self.parseOptions(parseFrom)
self.configuration = try Self.craft(diskPath, readOnly: readOnly, syncModeRaw: syncModeRaw, cachingModeRaw: cachingModeRaw)
self = try Self.craft(
diskPath,
readOnly: readOnly,
syncModeRaw: syncModeRaw,
cachingModeRaw: cachingModeRaw
)
}
static func craft(_ diskPath: String, readOnly diskReadOnly: Bool, syncModeRaw: String, cachingModeRaw: String) throws -> VZStorageDeviceConfiguration {
private init(configuration: VZStorageDeviceConfiguration, temporaryDiskLock: FileLock? = nil) {
self.configuration = configuration
self.temporaryDiskLock = temporaryDiskLock
}
private static func craft(
_ diskPath: String,
readOnly diskReadOnly: Bool,
syncModeRaw: String,
cachingModeRaw: String
) throws -> AdditionalDisk {
let diskURL = URL(string: diskPath)
if (["nbd", "nbds", "nbd+unix", "nbds+unix"].contains(diskURL?.scheme)) {
@@ -974,7 +997,7 @@ struct AdditionalDisk {
synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw)
)
return VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment)
return AdditionalDisk(configuration: VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment))
}
// Expand the tilde (~) since at this point we're dealing with a local path,
@@ -1005,13 +1028,33 @@ struct AdditionalDisk {
let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd, closeOnDealloc: true),
readOnly: diskReadOnly, synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw))
return VZVirtioBlockDeviceConfiguration(attachment: blockAttachment)
return AdditionalDisk(configuration: VZVirtioBlockDeviceConfiguration(attachment: blockAttachment))
}
// Support remote VM names in --disk command-line argument
if let remoteName = try? RemoteName(diskPath) {
let vmDir = try VMStorageOCI().open(remoteName)
if vmDir.isStackedCachedImage {
// A cached stacked image has no writable top overlay. Create one in a
// disposable directory for this additional-disk attachment.
let temporaryVMDir = try VMDirectory.temporary()
try FileManager.default.copyItem(at: vmDir.configURL, to: temporaryVMDir.configURL)
try FileManager.default.copyItem(at: vmDir.nvramURL, to: temporaryVMDir.nvramURL)
try FileManager.default.copyItem(at: vmDir.manifestURL, to: temporaryVMDir.manifestURL)
let lock = try FileLock(lockURL: temporaryVMDir.baseURL)
try lock.lock()
let stack = try temporaryVMDir.diskImageStack()
try stack.createWritableOverlay()
let attachment = try stack.makeAttachment(
readOnly: diskReadOnly,
cachingMode: try VZDiskImageCachingMode(cachingModeRaw) ?? .automatic,
synchronizationMode: try VZDiskImageSynchronizationMode(syncModeRaw)
)
return AdditionalDisk(configuration: VZVirtioBlockDeviceConfiguration(attachment: attachment), temporaryDiskLock: lock)
}
// Unfortunately, VZDiskImageStorageDeviceAttachment does not support
// FileHandle, so we can't easily clone the disk, open it and unlink(2)
// to simplify the garbage collection, so use an intermediate directory.
@@ -1024,7 +1067,7 @@ struct AdditionalDisk {
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: clonedDiskURL, readOnly: diskReadOnly)
return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
return AdditionalDisk(configuration: VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment), temporaryDiskLock: lock)
}
// Error out if the disk is locked by the host (e.g. it was mounted in Finder),
@@ -1040,7 +1083,7 @@ struct AdditionalDisk {
synchronizationMode: try VZDiskImageSynchronizationMode(syncModeRaw)
)
return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
return AdditionalDisk(configuration: VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment))
}
static func parseOptions(_ parseFrom: String) -> (String, Bool, String, String) {
+8
View File
@@ -39,6 +39,14 @@ struct Set: AsyncParsableCommand {
func run() async throws {
let vmDir = try VMStorageLocal().open(name)
// Replacing disk.img would leave a stacked VM with both disk.img and
// overlay.asif, which is not a supported local layout. Reject before
// saving any other requested configuration changes.
if disk != nil, vmDir.isStackedVM {
throw ValidationError("--disk is not supported for VMs with a stacked disk")
}
var vmConfig = try VMConfig(fromURL: vmDir.configURL)
if let cpu = cpu {
+46 -6
View File
@@ -39,15 +39,48 @@ struct ContentStore {
return targetURL.deletingLastPathComponent().appendingPathComponent(".\(UUID().uuidString).tmp")
}
/// Returns a validated cache hit. Corrupt files are treated as misses so a
/// later pull can safely rebuild them.
func existingContentURL(for contentDigest: String) throws -> URL? {
/// Returns a stable staging path so an interrupted registry pull can resume
/// reconstructing this content entry on a later attempt.
func resumableContentURL(for contentDigest: String) throws -> URL {
let targetURL = try contentURL(for: contentDigest)
return targetURL.deletingLastPathComponent().appendingPathComponent(".\(targetURL.lastPathComponent).partial")
}
/// Returns a stable lock file for serializing reconstruction of one content
/// entry. The file is intentionally retained; flock state lives on the file
/// descriptor and disappears when the owning process exits.
func lockURL(for contentDigest: String) throws -> URL {
let targetURL = try contentURL(for: contentDigest)
let lockURL = targetURL.deletingLastPathComponent().appendingPathComponent(".\(targetURL.lastPathComponent).lock")
if !FileManager.default.fileExists(atPath: lockURL.path) {
_ = FileManager.default.createFile(atPath: lockURL.path, contents: nil)
}
return lockURL
}
/// Returns a digest-addressed entry without rereading it. Pull verifies
/// content hashes before accepting a cache hit; clone only needs a cheap
/// structural check, like Tart's existing disk.img path.
func contentURLIfPresent(for contentDigest: String) throws -> URL? {
let url = try contentURL(for: contentDigest)
guard FileManager.default.fileExists(atPath: url.path) else {
return nil
}
return url
}
/// Returns a validated cache hit. Corrupt files are treated as misses so a
/// later pull can safely rebuild them.
func existingContentURL(for contentDigest: String) throws -> URL? {
guard let url = try contentURLIfPresent(for: contentDigest) else {
return nil
}
guard try Digest.hash(url) == contentDigest else {
return nil
}
@@ -57,7 +90,8 @@ struct ContentStore {
/// Move a fully reconstructed temporary file into the cache after verifying
/// its semantic identity. The caller should create the temporary file with
/// `temporaryContentURL(for:)` so rename stays on the same filesystem.
/// temporaryContentURL(for:) or resumableContentURL(for:) so rename stays on
/// the same filesystem.
func install(_ temporaryURL: URL, contentDigest: String) throws -> URL {
let actualDigest = try Digest.hash(temporaryURL)
guard actualDigest == contentDigest else {
@@ -65,14 +99,20 @@ struct ContentStore {
}
let targetURL = try contentURL(for: contentDigest)
let lock = try FileLock(lockURL: baseURL)
try lock.lock()
defer { try? lock.unlock() }
if let existingURL = try existingContentURL(for: contentDigest) {
try? FileManager.default.removeItem(at: temporaryURL)
return existingURL
}
try? FileManager.default.removeItem(at: targetURL)
try FileManager.default.moveItem(at: temporaryURL, to: targetURL)
if FileManager.default.fileExists(atPath: targetURL.path) {
_ = try FileManager.default.replaceItemAt(targetURL, withItemAt: temporaryURL)
} else {
try FileManager.default.moveItem(at: temporaryURL, to: targetURL)
}
return targetURL
}
+75 -39
View File
@@ -5,20 +5,17 @@ import Virtualization
import DiskImageKit
#endif
/// One immutable complete disk file used by a stacked disk.
///
/// This is a reconstructed base disk or published ASIF overlay, not an OCI
/// layer or an individual Tart disk chunk.
struct DiskImageFile {
let url: URL
let contentDigest: String
/// The logical block layout exposed by a disk image.
struct DiskImageBlockLayout {
let blockSize: UInt64
let blockCount: UInt64
}
enum DiskImageStackError: Error, Equatable, CustomStringConvertible {
case unavailable
case writableOverlayAlreadyExists(URL)
case writableOverlayMissing(URL)
case invalidGeometry(String)
case invalidBlockLayout(String)
case invalidDiskImage(URL, String)
var description: String {
@@ -29,7 +26,7 @@ enum DiskImageStackError: Error, Equatable, CustomStringConvertible {
"writable overlay already exists: \(url.path)"
case .writableOverlayMissing(let url):
"writable overlay is missing: \(url.path)"
case .invalidGeometry(let reason):
case .invalidBlockLayout(let reason):
reason
case .invalidDiskImage(let url, let reason):
"\(reason): \(url.path)"
@@ -38,16 +35,66 @@ enum DiskImageStackError: Error, Equatable, CustomStringConvertible {
}
struct DiskImageStack {
/// DiskImageKit-ready paths and geometry after Tart disk chunks have been
/// DiskImageKit-ready paths and block layout after Tart disk chunks have been
/// reconstructed into complete immutable files. The writable overlay stays
/// private to one VM.
let base: DiskImageFile
let baseURL: URL
let baseFormat: DiskImageFormat
let overlays: [DiskImageFile]
let immutableOverlayURLs: [URL]
let writableOverlayURL: URL
let blockSize: UInt64
let blockCount: UInt64
/// Reads a disk image's current block layout without resolving or validating a
/// whole stack. This is used for the VM's private writable overlay, whose
/// size may be newer than the pinned immutable parent manifest.
static func diskImageBlockLayout(at url: URL) throws -> DiskImageBlockLayout {
#if canImport(DiskImageKit)
if #available(macOS 27.0, *) {
let image = try DiskImage(opening: .open(url: url, mode: .readOnly))
return DiskImageBlockLayout(
blockSize: UInt64(image.blockSize.rawValue),
blockCount: UInt64(image.blockCount)
)
}
#endif
throw DiskImageStackError.unavailable
}
static func baseBlockLayout(
at url: URL,
expectedFormat: DiskImageFormat
) throws -> DiskImageBlockLayout {
#if canImport(DiskImageKit)
if #available(macOS 27.0, *) {
let image = try DiskImage(opening: .open(url: url, mode: .readOnly))
let matchesFormat = switch expectedFormat {
case .raw:
image.format == .raw
case .asif:
image.format == .asif
}
guard matchesFormat else {
throw DiskImageStackError.invalidDiskImage(url, "base disk format does not match")
}
guard image.layerType == nil, image.parentUUID == nil else {
throw DiskImageStackError.invalidDiskImage(url, "base disk must not be an overlay")
}
if expectedFormat == .asif && image.layerUUID == nil {
throw DiskImageStackError.invalidDiskImage(url, "ASIF base disk is missing a UUID")
}
return DiskImageBlockLayout(
blockSize: UInt64(image.blockSize.rawValue),
blockCount: UInt64(image.blockCount)
)
}
#endif
throw DiskImageStackError.unavailable
}
func createWritableOverlay() throws {
#if canImport(DiskImageKit)
if #available(macOS 27.0, *) {
@@ -68,12 +115,14 @@ struct DiskImageStack {
}
func makeAttachment(
readOnly: Bool = false,
cachingMode: VZDiskImageCachingMode = .automatic,
synchronizationMode: VZDiskImageSynchronizationMode = .full
) throws -> VZDiskImageStorageDeviceAttachment {
) throws -> VZStorageDeviceAttachment {
#if canImport(DiskImageKit)
if #available(macOS 27.0, *) {
return try attachmentWithDiskImageKit(
readOnly: readOnly,
cachingMode: cachingMode,
synchronizationMode: synchronizationMode
)
@@ -108,6 +157,7 @@ struct DiskImageStack {
@available(macOS 27.0, *)
private func attachmentWithDiskImageKit(
readOnly: Bool,
cachingMode: VZDiskImageCachingMode,
synchronizationMode: VZDiskImageSynchronizationMode
) throws -> VZDiskImageStorageDeviceAttachment {
@@ -118,7 +168,7 @@ struct DiskImageStack {
let parent = try validatedParentImage()
let writableOverlay = try openOverlay(
at: writableOverlayURL,
mode: .readWrite
mode: readOnly ? .readOnly : .readWrite
)
let stackedImage = try append(writableOverlay, to: parent, at: writableOverlayURL)
try validateAppendedOverlay(stackedImage, at: writableOverlayURL)
@@ -133,7 +183,7 @@ struct DiskImageStack {
@available(macOS 27.0, *)
private func growWritableOverlayWithDiskImageKit(toBlockCount blockCount: UInt64) throws {
guard blockCount > 0, let desiredBlockCount = Int(exactly: blockCount) else {
throw DiskImageStackError.invalidGeometry("invalid stacked disk block count \(blockCount)")
throw DiskImageStackError.invalidBlockLayout("invalid stacked disk block count \(blockCount)")
}
let parent = try validatedParentImage()
@@ -160,31 +210,29 @@ struct DiskImageStack {
private func validatedParentImage() throws -> DiskImage {
let expectedBlockSize = try diskImageBlockSize(blockSize)
guard blockCount > 0, let expectedBlockCount = Int(exactly: blockCount) else {
throw DiskImageStackError.invalidGeometry("invalid stacked disk block count \(blockCount)")
throw DiskImageStackError.invalidBlockLayout("invalid stacked disk block count \(blockCount)")
}
try verifyContentDigest(base)
let baseImage = try DiskImage(opening: .open(url: base.url, mode: .readOnly))
try validateBase(baseImage, at: base.url, expectedFormat: baseFormat)
let baseImage = try DiskImage(opening: .open(url: baseURL, mode: .readOnly))
try validateBase(baseImage, at: baseURL, expectedFormat: baseFormat)
var image = baseImage
for overlay in overlays {
for overlayURL in immutableOverlayURLs {
let openedOverlay = try openOverlay(
at: overlay.url,
expectedDigest: overlay.contentDigest,
at: overlayURL,
mode: .readOnly
)
let stackedImage = try append(openedOverlay, to: image, at: overlay.url)
try validateAppendedOverlay(stackedImage, at: overlay.url)
let stackedImage = try append(openedOverlay, to: image, at: overlayURL)
try validateAppendedOverlay(stackedImage, at: overlayURL)
image = stackedImage
}
guard image.blockSize == expectedBlockSize else {
throw DiskImageStackError.invalidGeometry("immutable disk stack does not match manifest block size")
throw DiskImageStackError.invalidBlockLayout("immutable disk stack does not match manifest block size")
}
guard image.blockCount == expectedBlockCount else {
throw DiskImageStackError.invalidGeometry("immutable disk stack does not match manifest block count")
throw DiskImageStackError.invalidBlockLayout("immutable disk stack does not match manifest block count")
}
return image
@@ -216,13 +264,8 @@ struct DiskImageStack {
@available(macOS 27.0, *)
private func openOverlay(
at url: URL,
expectedDigest: String? = nil,
mode: OpenConfiguration.Mode
) throws -> DiskImage {
if let expectedDigest {
try verifyContentDigest(DiskImageFile(url: url, contentDigest: expectedDigest))
}
let image = try DiskImage(opening: .open(url: url, mode: mode))
guard image.format == .asif else {
throw DiskImageStackError.invalidDiskImage(url, "overlay must use ASIF format")
@@ -247,17 +290,10 @@ struct DiskImageStack {
}
}
@available(macOS 27.0, *)
private func verifyContentDigest(_ diskImage: DiskImageFile) throws {
guard try Digest.hash(diskImage.url) == diskImage.contentDigest else {
throw DiskImageStackError.invalidDiskImage(diskImage.url, "disk image content digest does not match")
}
}
@available(macOS 27.0, *)
private func diskImageBlockSize(_ value: UInt64) throws -> DiskImage.BlockSize {
guard let intValue = Int(exactly: value), let blockSize = DiskImage.BlockSize(rawValue: intValue) else {
throw DiskImageStackError.invalidGeometry("unsupported stacked disk block size \(value)")
throw DiskImageStackError.invalidBlockLayout("unsupported stacked disk block size \(value)")
}
return blockSize
+45 -7
View File
@@ -7,6 +7,8 @@ enum DigestError: Error {
}
class Digest {
private static let fileBufferSize = 4 * 1024 * 1024
var hash: SHA256 = SHA256()
func update(_ data: Data) {
@@ -22,7 +24,10 @@ class Digest {
}
static func hash(_ url: URL) throws -> String {
hash(try Data(contentsOf: url))
let file = try FileHandle(forReadingFrom: url)
defer { try? file.close() }
return try hashContents(from: file)
}
static func hash(_ url: URL, offset: UInt64, size: UInt64) throws -> String {
@@ -36,20 +41,53 @@ class Digest {
throw DigestError.InvalidOffset
}
if (offset + size) > fileSize {
if size > fileSize - offset {
throw DigestError.InvalidSize
}
// Read a chunk of size ``size`` at offset ``offset``
// and calculate it's digest
// Read the requested range incrementally and calculate its digest.
let fh = try FileHandle(forReadingFrom: url)
defer { try! fh.close() }
defer { try? fh.close() }
try fh.seek(toOffset: offset)
let data = try fh.read(upToCount: Int(size))!
return try hashContents(from: fh, size: size)
}
return hash(data)
/// Streams a file into SHA-256 while keeping Foundation's temporary read
/// buffers scoped to one chunk.
private static func hashContents(from file: FileHandle, size: UInt64? = nil) throws -> String {
let digest = Digest()
var remaining = size
while remaining.map({ $0 > 0 }) ?? true {
let didRead = try autoreleasepool { () throws -> Bool in
let count = remaining.map {
Int(min(UInt64(fileBufferSize), $0))
} ?? fileBufferSize
guard let data = try file.read(upToCount: count), !data.isEmpty else {
if remaining != nil {
throw DigestError.InvalidSize
}
return false
}
digest.update(data)
if let bytesRemaining = remaining {
remaining = bytesRemaining - UInt64(data.count)
}
return true
}
if !didRead {
break
}
}
return digest.finalize()
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import Foundation
protocol Disk {
static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer]
static func push(diskURL: URL, mediaType: String, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer]
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws
}
+9 -2
View File
@@ -22,7 +22,14 @@ class DiskV2: Disk {
private static let holeGranularityBytes = 4 * 1024 * 1024
private static let zeroChunk = Data(count: holeGranularityBytes)
static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer] {
static func push(
diskURL: URL,
mediaType: String,
registry: Registry,
chunkSizeMb: Int,
concurrency: UInt,
progress: Progress
) async throws -> [OCIManifestLayer] {
var pushedLayers: [(index: Int, pushedLayer: OCIManifestLayer)] = []
// Open the disk file
@@ -63,7 +70,7 @@ class DiskV2: Disk {
progress.completedUnitCount += Int64(data.count)
return (index, OCIManifestLayer(
mediaType: diskV2MediaType,
mediaType: mediaType,
size: compressedData.count,
digest: compressedDataDigest,
uncompressedSize: UInt64(data.count),
+23 -12
View File
@@ -64,7 +64,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Initialize the virtual machine and its configuration
self.network = network
configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
configuration = try Self.craftConfiguration(vmDir: vmDir,
nvramURL: vmDir.nvramURL, vmConfig: config,
network: network, additionalStorageDevices: additionalStorageDevices,
directorySharingDevices: directorySharingDevices,
@@ -196,7 +196,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Initialize the virtual machine and its configuration
self.network = network
configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
configuration = try Self.craftConfiguration(vmDir: vmDir,
nvramURL: vmDir.nvramURL,
vmConfig: config, network: network,
additionalStorageDevices: additionalStorageDevices,
directorySharingDevices: directorySharingDevices,
@@ -312,7 +313,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
static func craftConfiguration(
diskURL: URL,
vmDir: VMDirectory,
nvramURL: URL,
vmConfig: VMConfig,
network: Network = NetworkShared(),
@@ -404,15 +405,25 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
// Storage
let attachment = try VZDiskImageStorageDeviceAttachment(
url: diskURL,
readOnly: false,
// When not specified, use "cached" caching mode for Linux VMs to prevent file-system corruption[1]
//
// [1]: https://github.com/cirruslabs/tart/pull/675
cachingMode: caching ?? (vmConfig.os == .linux ? .cached : .automatic),
synchronizationMode: sync
)
// When not specified, use "cached" caching mode for Linux VMs to prevent file-system corruption[1]
//
// [1]: https://github.com/cirruslabs/tart/pull/675
let cachingMode = caching ?? (vmConfig.os == .linux ? .cached : .automatic)
let attachment: VZStorageDeviceAttachment
if vmDir.isStackedVM {
attachment = try vmDir.diskImageStack().makeAttachment(
readOnly: false,
cachingMode: cachingMode,
synchronizationMode: sync
)
} else {
attachment = try VZDiskImageStorageDeviceAttachment(
url: vmDir.diskURL,
readOnly: false,
cachingMode: cachingMode,
synchronizationMode: sync
)
}
var devices: [VZStorageDeviceConfiguration] = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
devices.append(contentsOf: additionalStorageDevices)
+4
View File
@@ -10,6 +10,10 @@ fileprivate let permissions = FilePermissions(rawValue: 0o644)
// [2]: https://developer.apple.com/documentation/compression/algorithm/lzfse
extension VMDirectory {
func exportToArchive(path: String) throws {
guard !isStackedVM && !isStackedCachedImage else {
throw RuntimeError.ExportFailed("exporting stacked VMs is not supported yet")
}
guard let fileStream = ArchiveByteStream.fileStream(
path: FilePath(path),
mode: .writeOnly,
@@ -0,0 +1,125 @@
import Foundation
extension VMDirectory {
func diskImageStack(contentStore providedStore: ContentStore? = nil) throws -> DiskImageStack {
let manifest = try OCIManifest(fromJSON: Data(contentsOf: manifestURL))
let base: TartDiskFileGroup
let overlays: [TartDiskFileGroup]
switch try manifest.tartDiskRepresentation() {
case .flat(let pinnedBase) where pinnedBase.contentDigest != nil:
base = pinnedBase
overlays = []
case .stacked(let stackedBase, let stackedOverlays):
base = stackedBase
overlays = stackedOverlays
default:
throw RuntimeError.VMConfigurationError("VM is missing its disk image metadata")
}
guard let blockSize = manifest.diskBlockSize(),
let blockCount = manifest.diskBlockCount() else {
throw DiskImageStackError.invalidBlockLayout("disk image metadata is missing block layout")
}
let contentStore = try providedStore ?? ContentStore()
let baseURL = try diskImageURL(for: base, contentStore: contentStore)
let immutableOverlayURLs = try overlays.map { try diskImageURL(for: $0, contentStore: contentStore) }
let config = try VMConfig(fromURL: configURL)
return DiskImageStack(
baseURL: baseURL,
baseFormat: config.diskFormat,
immutableOverlayURLs: immutableOverlayURLs,
writableOverlayURL: overlayURL,
blockSize: blockSize,
blockCount: blockCount
)
}
func cloneStacked(
to destination: VMDirectory,
copyWritableOverlay: Bool,
generateMAC: Bool,
contentStore: ContentStore? = nil
) throws {
try FileManager.default.copyItem(at: configURL, to: destination.configURL)
try FileManager.default.copyItem(at: nvramURL, to: destination.nvramURL)
try FileManager.default.copyItem(at: manifestURL, to: destination.manifestURL)
if copyWritableOverlay {
try FileManager.default.copyItem(at: overlayURL, to: destination.overlayURL)
} else {
try destination.diskImageStack(contentStore: contentStore).createWritableOverlay()
}
if generateMAC {
try destination.regenerateMACAddress()
}
}
func cloneAsStackedBase(
to destination: VMDirectory,
generateMAC: Bool,
contentStore providedStore: ContentStore? = nil
) throws {
let config = try VMConfig(fromURL: configURL)
let blockLayout = try DiskImageStack.baseBlockLayout(at: diskURL, expectedFormat: config.diskFormat)
let contentDigest = try Digest.hash(diskURL)
let contentStore = try providedStore ?? ContentStore()
if try contentStore.existingContentURL(for: contentDigest) == nil {
let temporaryURL = try contentStore.temporaryContentURL(for: contentDigest)
do {
try FileManager.default.copyItem(at: diskURL, to: temporaryURL)
_ = try contentStore.install(temporaryURL, contentDigest: contentDigest)
} catch {
try? FileManager.default.removeItem(at: temporaryURL)
throw error
}
}
var manifest = try OCIManifest(fromJSON: Data(contentsOf: manifestURL))
guard case .flat = try manifest.tartDiskRepresentation() else {
throw RuntimeError.VMConfigurationError("--stacked cannot use an image that already has a stacked disk")
}
guard let firstDiskIndex = manifest.layers.firstIndex(where: { $0.mediaType == diskV2MediaType }) else {
throw OCIManifestValidationError.invalidLayout("manifest must contain at least one disk chunk")
}
var baseAnnotations = manifest.layers[firstDiskIndex].annotations ?? [:]
baseAnnotations[diskFileContentDigestAnnotation] = contentDigest
manifest.layers[firstDiskIndex].annotations = baseAnnotations
let diskSize = blockLayout.blockSize.multipliedReportingOverflow(by: blockLayout.blockCount)
guard !diskSize.overflow else {
throw DiskImageStackError.invalidBlockLayout("stacked disk block layout overflows UInt64")
}
var annotations = manifest.annotations ?? [:]
annotations[diskBlockSizeAnnotation] = String(blockLayout.blockSize)
annotations[uncompressedDiskSizeAnnotation] = String(diskSize.partialValue)
manifest.annotations = annotations
try FileManager.default.copyItem(at: configURL, to: destination.configURL)
try FileManager.default.copyItem(at: nvramURL, to: destination.nvramURL)
try manifest.toJSON().write(to: destination.manifestURL)
try destination.diskImageStack(contentStore: contentStore).createWritableOverlay()
if generateMAC {
try destination.regenerateMACAddress()
}
}
private func diskImageURL(for group: TartDiskFileGroup, contentStore: ContentStore) throws -> URL {
guard let contentDigest = group.contentDigest else {
throw OCIManifestValidationError.invalidDiskMetadata("stacked disk files need a whole-file content digest")
}
// Pull/install verifies immutable content before publishing it. Clone and
// run use the trusted content-addressed entry without rereading a possibly
// very large disk file, matching Tart's existing disk.img behavior.
guard let url = try contentStore.contentURLIfPresent(for: contentDigest) else {
throw RuntimeError.VMMissingFiles("VM is missing cached disk content \(contentDigest)")
}
return url
}
}
+247 -33
View File
@@ -6,7 +6,6 @@ let legacyDiskV1MediaType = "application/vnd.cirruslabs.tart.disk.v1"
enum OCIError: Error {
case ShouldBeExactlyOneLayer
case ShouldBeAtLeastOneLayer
case FailedToCreateVmFile
case LayerIsMissingUncompressedSizeAnnotation
case LayerIsMissingUncompressedDigestAnnotation
@@ -14,7 +13,7 @@ enum OCIError: Error {
extension VMDirectory {
func pullFromRegistry(registry: Registry, manifest: OCIManifest, concurrency: UInt, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws {
// Pull VM's config file layer and re-serialize it into a config file
// Pull VM's config file layer and store it as the local config file.
let configLayers = manifest.layers.filter {
$0.mediaType == configMediaType
}
@@ -30,17 +29,22 @@ extension VMDirectory {
}
try configFile.close()
// Pull VM's disk layers and decompress them into a disk file
// Pull VM's disk chunks and decompress them into complete disk files.
if manifest.layers.contains(where: { $0.mediaType == legacyDiskV1MediaType }) {
throw RuntimeError.Generic("Pulling OCI images with legacy disk media type \(legacyDiskV1MediaType) is no longer supported, please re-push the image using a current Tart version")
}
let layers = manifest.layers.filter { $0.mediaType == diskV2MediaType }
if layers.isEmpty {
throw OCIError.ShouldBeAtLeastOneLayer
let diskRepresentation = try manifest.tartDiskRepresentation()
let diskChunks: [OCIManifestLayer]
switch diskRepresentation {
case .flat(let base):
diskChunks = base.chunks
case .stacked(let base, let overlays):
diskChunks = base.chunks + overlays.flatMap(\.chunks)
}
let diskCompressedSize = layers.map { Int64($0.size) }.reduce(0, +)
let diskCompressedSize = diskChunks.map { Int64($0.size) }.reduce(0, +)
OpenTelemetry.instance.contextProvider.activeSpan?.setAttribute(
key: "compressed_disk_size_bytes",
value: .int(Int(diskCompressedSize))
@@ -53,19 +57,42 @@ extension VMDirectory {
ProgressObserver(progress).log(defaultLogger)
do {
try await DiskV2.pull(registry: registry, diskLayers: layers, diskURL: diskURL,
concurrency: concurrency, progress: progress,
localLayerCache: localLayerCache,
deduplicate: deduplicate)
switch diskRepresentation {
case .flat(let base):
try await DiskV2.pull(registry: registry, diskLayers: base.chunks, diskURL: diskURL,
concurrency: concurrency, progress: progress,
localLayerCache: localLayerCache,
deduplicate: deduplicate)
if deduplicate, let llc = localLayerCache {
// set custom attribute to remember deduplicated bytes
diskURL.setDeduplicatedBytes(llc.deduplicatedBytes)
}
case .stacked(let base, let overlays):
// The deterministic resumable directory may contain a partial
// disk.img from an interrupted pull while this tag was standalone. A
// cached stacked image must not retain that file or it is mistaken for
// a standalone VM after the pull is moved into cache.
if FileManager.default.fileExists(atPath: diskURL.path) {
try FileManager.default.removeItem(at: diskURL)
}
let contentStore = try ContentStore()
for group in [base] + overlays {
_ = try await pullDiskFile(
registry: registry,
group: group,
contentStore: contentStore,
concurrency: concurrency,
progress: progress
)
}
}
} catch let error where error is FilterError {
throw RuntimeError.PullFailed("failed to decompress disk: \(error.localizedDescription)")
}
if deduplicate, let llc = localLayerCache {
// set custom attribute to remember deduplicated bytes
diskURL.setDeduplicatedBytes(llc.deduplicatedBytes)
}
// Pull VM's NVRAM file layer and store it in an NVRAM file
defaultLogger.appendNewLine("pulling NVRAM...")
@@ -83,12 +110,47 @@ extension VMDirectory {
try nvram.write(contentsOf: data)
}
try nvram.close()
// Serialize VM's manifest to enable better deduplication on subsequent "tart pull"'s
try manifest.toJSON().write(to: manifestURL)
}
func pushToRegistry(registry: Registry, references: [String], chunkSizeMb: Int, concurrency: UInt, labels: [String: String] = [:]) async throws -> RemoteName {
/// Reconstructs one complete immutable base disk or published ASIF overlay
/// from its Tart disk chunks, unless the shared content store already has a
/// verified copy.
private func pullDiskFile(
registry: Registry,
group: TartDiskFileGroup,
contentStore: ContentStore,
concurrency: UInt,
progress: Progress
) async throws -> URL {
guard let contentDigest = group.contentDigest else {
throw OCIManifestValidationError.invalidDiskMetadata("stacked disk files need a whole-file content digest")
}
// Pulls for the same semantic disk file share a stable resumable path so
// DiskV2 can resume after a transient failure. Serialize writers before
// rechecking the final entry to avoid racing on that shared path.
let lock = try FileLock(lockURL: contentStore.lockURL(for: contentDigest))
try lock.lock()
defer { try? lock.unlock() }
if let existingURL = try contentStore.existingContentURL(for: contentDigest) {
progress.completedUnitCount += group.chunks.reduce(0) { $0 + Int64($1.size) }
return existingURL
}
let resumableURL = try contentStore.resumableContentURL(for: contentDigest)
try await DiskV2.pull(
registry: registry,
diskLayers: group.chunks,
diskURL: resumableURL,
concurrency: concurrency,
progress: progress
)
return try contentStore.install(resumableURL, contentDigest: contentDigest)
}
func pushToRegistry(registry: Registry, references: [String], chunkSizeMb: Int, concurrency: UInt, labels: [String: String] = [:]) async throws -> (name: RemoteName, manifest: OCIManifest) {
var layers = Array<OCIManifestLayer>()
// Read VM's config and push it as blob
@@ -102,14 +164,12 @@ extension VMDirectory {
let configDigest = try await registry.pushBlob(fromData: configJSON, chunkSizeMb: chunkSizeMb)
layers.append(OCIManifestLayer(mediaType: configMediaType, size: configJSON.count, digest: configDigest))
// Compress the disk file as multiple chunks and push them as disk layers
let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64
defaultLogger.appendNewLine("pushing disk... this will take a while...")
let progress = Progress(totalUnitCount: diskSize)
ProgressObserver(progress).log(defaultLogger)
layers.append(contentsOf: try await DiskV2.push(diskURL: diskURL, registry: registry, chunkSizeMb: chunkSizeMb, concurrency: concurrency, progress: progress))
let (diskLayers, diskAnnotations) = try await pushDiskLayers(
registry: registry,
chunkSizeMb: chunkSizeMb,
concurrency: concurrency
)
layers.append(contentsOf: diskLayers)
// Read VM's NVRAM and push it as blob
defaultLogger.appendNewLine("pushing NVRAM...")
@@ -122,12 +182,13 @@ extension VMDirectory {
let ociConfigContainer = OCIConfig.ConfigContainer(Labels: labels)
let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os, config: ociConfigContainer).toJSON()
let ociConfigDigest = try await registry.pushBlob(fromData: ociConfigJSON, chunkSizeMb: chunkSizeMb)
let manifest = OCIManifest(
var manifest = OCIManifest(
config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest),
layers: layers,
uncompressedDiskSize: UInt64(diskSize),
uploadDate: Date()
layers: layers
)
var annotations = diskAnnotations
annotations[uploadTimeAnnotation] = Date().toISO()
manifest.annotations = annotations
// Manifest
for reference in references {
@@ -137,7 +198,160 @@ extension VMDirectory {
}
let pushedReference = Reference(digest: try manifest.digest())
return RemoteName(host: registry.host!, namespace: registry.namespace, reference: pushedReference)
let name = RemoteName(host: registry.host!, namespace: registry.namespace, reference: pushedReference)
return (name, manifest)
}
/// Builds the disk portion of the manifest. Registry transport is shared
/// for standalone and stacked VMs; only their local disk representation
/// determines which descriptors need to be uploaded or reused.
private func pushDiskLayers(
registry: Registry,
chunkSizeMb: Int,
concurrency: UInt
) async throws -> ([OCIManifestLayer], [String: String]) {
guard isStackedVM else {
let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64
defaultLogger.appendNewLine("pushing disk... this will take a while...")
let progress = Progress(totalUnitCount: diskSize)
ProgressObserver(progress).log(defaultLogger)
let layers = try await DiskV2.push(
diskURL: diskURL,
mediaType: diskV2MediaType,
registry: registry,
chunkSizeMb: chunkSizeMb,
concurrency: concurrency,
progress: progress
)
return (layers, [uncompressedDiskSizeAnnotation: String(diskSize)])
}
let localManifest = try OCIManifest(fromJSON: Data(contentsOf: manifestURL))
let inheritedGroups: [TartDiskFileGroup]
switch try localManifest.tartDiskRepresentation() {
case .flat(let base) where base.contentDigest != nil:
inheritedGroups = [base]
case .stacked(let base, let overlays):
inheritedGroups = [base] + overlays
default:
throw RuntimeError.VMConfigurationError("stacked VM is missing a pinned disk stack")
}
let contentStore = try ContentStore()
var layers: [OCIManifestLayer] = []
for group in inheritedGroups {
layers.append(contentsOf: try await descriptorsForCachedDiskFile(
group,
contentStore: contentStore,
registry: registry,
chunkSizeMb: chunkSizeMb,
concurrency: concurrency
))
}
// Keep the snapshot out of startup GC while this potentially long push
// hashes, uploads, and inspects it.
let frozenOverlayDirectory = try VMDirectory.temporary()
let frozenOverlayLock = try FileLock(lockURL: frozenOverlayDirectory.baseURL)
try frozenOverlayLock.lock()
defer {
try? frozenOverlayLock.unlock()
try? FileManager.default.removeItem(at: frozenOverlayDirectory.baseURL)
}
let frozenOverlayURL = frozenOverlayDirectory.baseURL.appendingPathComponent("overlay.asif")
try FileManager.default.copyItem(at: overlayURL, to: frozenOverlayURL)
let overlaySize = try FileManager.default.attributesOfItem(atPath: frozenOverlayURL.path)[.size] as! Int64
defaultLogger.appendNewLine("pushing overlay...")
let progress = Progress(totalUnitCount: overlaySize)
ProgressObserver(progress).log(defaultLogger)
let contentDigest = try Digest.hash(frozenOverlayURL)
let chunks = try await DiskV2.push(
diskURL: frozenOverlayURL,
mediaType: asifOverlayMediaType,
registry: registry,
chunkSizeMb: chunkSizeMb,
concurrency: concurrency,
progress: progress
)
layers.append(contentsOf: annotatedChunks(chunks, kind: .asifOverlay, contentDigest: contentDigest))
let blockLayout = try DiskImageStack.diskImageBlockLayout(at: frozenOverlayURL)
let diskSize = blockLayout.blockSize.multipliedReportingOverflow(by: blockLayout.blockCount)
guard !diskSize.overflow else {
throw DiskImageStackError.invalidBlockLayout("stacked disk block layout overflows UInt64")
}
var annotations = localManifest.annotations ?? [:]
annotations[diskBlockSizeAnnotation] = String(blockLayout.blockSize)
annotations[uncompressedDiskSizeAnnotation] = String(diskSize.partialValue)
return (layers, annotations)
}
/// Returns transport descriptors for an immutable disk file. If the
/// target registry lacks the original blobs, recreate them from the local
/// content store.
private func descriptorsForCachedDiskFile(
_ group: TartDiskFileGroup,
contentStore: ContentStore,
registry: Registry,
chunkSizeMb: Int,
concurrency: UInt
) async throws -> [OCIManifestLayer] {
guard let contentDigest = group.contentDigest else {
throw RuntimeError.VMConfigurationError("stacked VM is missing a pinned disk file digest")
}
var allChunksExist = true
for chunk in group.chunks {
if try await !registry.blobExists(chunk.digest) {
allChunksExist = false
break
}
}
if allChunksExist {
return group.chunks
}
guard let contentURL = try contentStore.existingContentURL(for: contentDigest) else {
throw RuntimeError.VMMissingFiles("stacked VM is missing cached disk content \(contentDigest)")
}
let contentSize = try FileManager.default.attributesOfItem(atPath: contentURL.path)[.size] as! Int64
let progress = Progress(totalUnitCount: contentSize)
let mediaType = group.kind == .base ? diskV2MediaType : asifOverlayMediaType
let chunks = try await DiskV2.push(
diskURL: contentURL,
mediaType: mediaType,
registry: registry,
chunkSizeMb: chunkSizeMb,
concurrency: concurrency,
progress: progress
)
return annotatedChunks(chunks, kind: group.kind, contentDigest: contentDigest)
}
private func annotatedChunks(
_ chunks: [OCIManifestLayer],
kind: TartDiskFileGroup.Kind,
contentDigest: String
) -> [OCIManifestLayer] {
guard !chunks.isEmpty else {
return chunks
}
var chunks = chunks
var annotations = chunks[0].annotations ?? [:]
annotations[diskFileContentDigestAnnotation] = contentDigest
if kind == .asifOverlay {
annotations[diskFileChunkCountAnnotation] = String(chunks.count)
}
chunks[0].annotations = annotations
return chunks
}
}
+98 -4
View File
@@ -142,6 +142,24 @@ struct VMDirectory: Prunable {
layout?.isRunnable == true
}
var isStandalone: Bool {
layout == .standalone
}
var isStackedVM: Bool {
layout == .stackedLocal
}
var isStackedCachedImage: Bool {
layout == .stackedOCIRecord
}
/// Shapes that may live in the remote-image cache. A cached stacked image
/// has no writable overlay and is intentionally not runnable as a local VM.
var isCachedImage: Bool {
layout == .standalone || layout == .stackedOCIRecord
}
func initialize(overwrite: Bool = false) throws {
if !overwrite && initialized {
throw RuntimeError.VMDirectoryAlreadyInitialized("VM directory is already initialized, preventing overwrite")
@@ -172,6 +190,20 @@ struct VMDirectory: Prunable {
}
}
func validateCachedImage(userFriendlyName: String) throws {
if !FileManager.default.fileExists(atPath: baseURL.path) {
throw RuntimeError.VMDoesNotExist(name: userFriendlyName)
}
if !isCachedImage {
throw RuntimeError.VMMissingFiles(
"cached image is missing files for a supported layout: "
+ "standalone requires \(configURL.lastPathComponent), \(diskURL.lastPathComponent) and \(nvramURL.lastPathComponent); "
+ "stacked requires \(configURL.lastPathComponent), \(manifestURL.lastPathComponent) and \(nvramURL.lastPathComponent)"
)
}
}
func clone(to: VMDirectory, generateMAC: Bool) throws {
try FileManager.default.copyItem(at: configURL, to: to.configURL)
try FileManager.default.copyItem(at: nvramURL, to: to.nvramURL)
@@ -198,7 +230,26 @@ struct VMDirectory: Prunable {
try vmConfig.save(toURL: configURL)
}
func resizeDisk(_ sizeGB: UInt16, format: DiskImageFormat = .raw) throws {
func resizeDisk(
_ sizeGB: UInt16,
format: DiskImageFormat = .raw,
contentStore: ContentStore? = nil
) throws {
if isStackedVM {
guard try state() == .Stopped else {
throw RuntimeError.VMConfigurationError("VM \"\(name)\" must be stopped before resizing its disk")
}
let stack = try diskImageStack(contentStore: contentStore)
let desiredSizeBytes = UInt64(sizeGB) * 1000 * 1000 * 1000
guard desiredSizeBytes.isMultiple(of: stack.blockSize) else {
throw RuntimeError.InvalidDiskSize("new disk size must align to the stacked disk block size")
}
try stack.growWritableOverlay(toBlockCount: desiredSizeBytes / stack.blockSize)
return
}
let diskExists = FileManager.default.fileExists(atPath: diskURL.path)
if diskExists {
@@ -332,7 +383,7 @@ struct VMDirectory: Prunable {
}
func allocatedSizeBytes() throws -> Int {
try configURL.allocatedSizeBytes() + diskURL.allocatedSizeBytes() + nvramURL.allocatedSizeBytes()
try configURL.allocatedSizeBytes() + localDiskStorageAllocatedSizeBytes() + nvramURL.allocatedSizeBytes()
}
func allocatedSizeGB() throws -> Int {
@@ -340,7 +391,7 @@ struct VMDirectory: Prunable {
}
func deduplicatedSizeBytes() throws -> Int {
try configURL.deduplicatedSizeBytes() + diskURL.deduplicatedSizeBytes() + nvramURL.deduplicatedSizeBytes()
try configURL.deduplicatedSizeBytes() + localDiskStorageDeduplicatedSizeBytes() + nvramURL.deduplicatedSizeBytes()
}
func deduplicatedSizeGB() throws -> Int {
@@ -348,7 +399,7 @@ struct VMDirectory: Prunable {
}
func sizeBytes() throws -> Int {
try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes()
try configURL.sizeBytes() + localDiskStorageSizeBytes() + nvramURL.sizeBytes()
}
func sizeGB() throws -> Int {
@@ -356,6 +407,30 @@ struct VMDirectory: Prunable {
}
func diskSizeBytes() throws -> Int {
if isStackedVM {
let blockLayout = try DiskImageStack.diskImageBlockLayout(at: overlayURL)
let product = blockLayout.blockSize.multipliedReportingOverflow(by: blockLayout.blockCount)
guard !product.overflow, let diskSizeBytes = Int(exactly: product.partialValue) else {
throw RuntimeError.VMConfigurationError("VM has invalid stacked disk block layout")
}
return diskSizeBytes
}
if isStackedCachedImage {
let manifest = try OCIManifest(fromJSON: Data(contentsOf: manifestURL))
guard let blockSize = manifest.diskBlockSize(),
let blockCount = manifest.diskBlockCount() else {
throw RuntimeError.VMConfigurationError("VM has invalid stacked disk block layout")
}
let product = blockSize.multipliedReportingOverflow(by: blockCount)
guard !product.overflow, let diskSizeBytes = Int(exactly: product.partialValue) else {
throw RuntimeError.VMConfigurationError("VM has invalid stacked disk block layout")
}
return diskSizeBytes
}
let vmConfig = try VMConfig(fromURL: configURL)
return switch vmConfig.diskFormat {
@@ -377,4 +452,23 @@ struct VMDirectory: Prunable {
func isExplicitlyPulled() -> Bool {
FileManager.default.fileExists(atPath: explicitlyPulledMark.path)
}
private var localDiskStorageURL: URL {
isStackedVM ? overlayURL : diskURL
}
// Cached stacked images own no disk file in their VM directory. Their
// immutable disk content lives in the shared content store and must not be
// charged to every cached image that references it.
private func localDiskStorageAllocatedSizeBytes() throws -> Int {
isStackedCachedImage ? 0 : try localDiskStorageURL.allocatedSizeBytes()
}
private func localDiskStorageDeduplicatedSizeBytes() throws -> Int {
isStackedCachedImage ? 0 : try localDiskStorageURL.deduplicatedSizeBytes()
}
private func localDiskStorageSizeBytes() throws -> Int {
isStackedCachedImage ? 0 : try localDiskStorageURL.sizeBytes()
}
}
+293 -21
View File
@@ -18,7 +18,109 @@ class VMStorageOCI: PrunableStorage {
}
func exists(_ name: RemoteName) -> Bool {
VMDirectory(baseURL: vmURL(name)).initialized
VMDirectory(baseURL: vmURL(name)).isCachedImage
}
/// Whether clone can use a cached image without pulling. Standalone images keep
/// Tart's existing structural check. Stacked cached images cheaply require every
/// immutable file with its expected length; explicit pull remains the path
/// that hashes content and repairs same-sized corruption.
func hasUsableCachedImageForClone(_ name: RemoteName, requireManifest: Bool = false) throws -> Bool {
guard exists(name) else {
return false
}
let vmDir = VMDirectory(baseURL: vmURL(name))
if requireManifest && !FileManager.default.fileExists(atPath: vmDir.manifestURL.path) {
return false
}
guard vmDir.isStackedCachedImage else {
return true
}
let manifest = try OCIManifest(fromJSON: Data(contentsOf: vmDir.manifestURL))
guard case .stacked(let base, let overlays) = try manifest.tartDiskRepresentation() else {
return true
}
let contentStore = try ContentStore()
for group in [base] + overlays {
guard let contentDigest = group.contentDigest,
let contentURL = try contentStore.contentURLIfPresent(for: contentDigest) else {
return false
}
var expectedSize: UInt64 = 0
for chunk in group.chunks {
guard let uncompressedSize = chunk.uncompressedSize() else {
return false
}
let addition = expectedSize.addingReportingOverflow(uncompressedSize)
guard !addition.overflow else {
return false
}
expectedSize = addition.partialValue
}
guard let actualSize = UInt64(exactly: try contentURL.sizeBytes()),
actualSize == expectedSize else {
return false
}
}
return true
}
/// Whether a cached image is complete enough for `pull` to return without
/// repairing it. Standalone images keep Tart's existing structural cache-hit
/// behavior; stacked cached images additionally need every immutable disk file in
/// the shared content store.
func hasCompleteCachedImage(_ name: RemoteName, manifest: OCIManifest) throws -> Bool {
guard exists(name) else {
return false
}
guard let missingGroups = try missingStackedDiskFileGroups(for: manifest) else {
return true
}
return missingGroups.isEmpty
}
/// The lock-free pull fast path is only useful for a tag that already
/// points at this digest. New or retargeted tags validate once after taking
/// the host lock instead of hashing a large stack twice.
func hasCompleteLinkedImage(_ name: RemoteName, digestName: RemoteName, manifest: OCIManifest) throws -> Bool {
guard exists(name), linked(from: name, to: digestName) else {
return false
}
return try hasCompleteCachedImage(digestName, manifest: manifest)
}
/// Bytes that this pull may need to materialize locally. For stacked images
/// this is the sum of only the missing complete disk files, not the final
/// guest-visible disk block layout.
func requiredDiskStorageBytes(for manifest: OCIManifest) throws -> UInt64? {
guard let missingGroups = try missingStackedDiskFileGroups(for: manifest) else {
return manifest.uncompressedDiskSize()
}
var total: UInt64 = 0
for group in missingGroups {
for chunk in group.chunks {
guard let uncompressedSize = chunk.uncompressedSize() else {
throw OCIManifestValidationError.invalidDiskMetadata("disk chunks need uncompressed size and content digest")
}
let addition = total.addingReportingOverflow(uncompressedSize)
guard !addition.overflow else {
throw RuntimeError.PullFailed("stacked disk storage size overflows UInt64")
}
total = addition.partialValue
}
}
return total
}
func digest(_ name: RemoteName) throws -> String {
@@ -34,7 +136,7 @@ class VMStorageOCI: PrunableStorage {
func open(_ name: RemoteName, _ accessDate: Date = Date()) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.validate(userFriendlyName: name.description)
try vmDir.validateCachedImage(userFriendlyName: name.description)
try vmDir.baseURL.updateAccessDate(accessDate)
@@ -44,11 +146,55 @@ class VMStorageOCI: PrunableStorage {
func create(_ name: RemoteName, overwrite: Bool = false) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
if !overwrite && vmDir.isCachedImage {
throw RuntimeError.VMDirectoryAlreadyInitialized("VM directory is already initialized, preventing overwrite")
}
try vmDir.initialize(overwrite: overwrite)
return vmDir
}
/// Materialize the digest-addressed cached image for an image Tart just
/// pushed, without routing its own local data back through the registry.
func populate(_ name: RemoteName, from source: VMDirectory, manifest: OCIManifest) throws {
if try hasCompleteCachedImage(name, manifest: manifest) {
return
}
let vmDir = try create(name, overwrite: exists(name))
if source.isStackedVM {
guard case .stacked(_, let overlays) = try manifest.tartDiskRepresentation(),
let contentDigest = overlays.last?.contentDigest else {
throw RuntimeError.VMConfigurationError("pushed image is missing its writable ASIF overlay")
}
// The pushed top overlay becomes immutable in the cached image. Keep a
// semantic copy so later clones do not need to fetch it back.
let contentStore = try ContentStore()
if try contentStore.existingContentURL(for: contentDigest) == nil {
let temporaryURL = try contentStore.temporaryContentURL(for: contentDigest)
do {
try FileManager.default.copyItem(at: source.overlayURL, to: temporaryURL)
_ = try contentStore.install(temporaryURL, contentDigest: contentDigest)
} catch {
try? FileManager.default.removeItem(at: temporaryURL)
throw error
}
}
try FileManager.default.copyItem(at: source.configURL, to: vmDir.configURL)
try FileManager.default.copyItem(at: source.nvramURL, to: vmDir.nvramURL)
} else {
try source.clone(to: vmDir, generateMAC: false)
}
// Keep the exact manifest Tart submitted so tag links and later pushes
// refer to the same digest-addressed cached image.
try manifest.toJSON().write(to: vmDir.manifestURL)
}
func move(_ name: RemoteName, from: VMDirectory) throws{
let targetURL = vmURL(name)
@@ -84,7 +230,7 @@ class VMStorageOCI: PrunableStorage {
}
let vmDir = VMDirectory(baseURL: foundURL.resolvingSymlinksInPath())
if !vmDir.initialized {
if !vmDir.isCachedImage {
continue
}
@@ -113,7 +259,7 @@ class VMStorageOCI: PrunableStorage {
for case let foundURL as URL in enumerator {
let vmDir = VMDirectory(baseURL: foundURL)
if !vmDir.initialized {
if !vmDir.isCachedImage {
continue
}
@@ -141,7 +287,9 @@ class VMStorageOCI: PrunableStorage {
}
func prunables() throws -> [Prunable] {
try list().filter { (_, _, isSymlink) in !isSymlink }.map { (_, vmDir, _) in vmDir }
try list().filter { (_, vmDir, isSymlink) in
!isSymlink && vmDir.isStandalone
}.map { (_, vmDir, _) in vmDir }
}
func pull(_ name: RemoteName, registry: Registry, concurrency: UInt, deduplicate: Bool) async throws {
@@ -157,7 +305,7 @@ class VMStorageOCI: PrunableStorage {
let digestName = RemoteName(host: name.host, namespace: name.namespace,
reference: Reference(digest: Digest.hash(manifestData)))
if exists(name) && exists(digestName) && linked(from: name, to: digestName) {
if try hasCompleteLinkedImage(name, digestName: digestName, manifest: manifest) {
// optimistically check if we need to do anything at all before locking
defaultLogger.appendNewLine("\(digestName) image is already cached and linked!")
return
@@ -181,11 +329,13 @@ class VMStorageOCI: PrunableStorage {
throw CancellationError()
}
if !exists(digestName) {
if try !hasCompleteCachedImage(digestName, manifest: manifest) {
let span = OTel.shared.tracer.spanBuilder(spanName: "pull").setActive(true).startSpan()
defer { span.end() }
let tmpVMDir = try VMDirectory.temporaryDeterministic(key: name.description)
let digestVMDir = VMDirectory(baseURL: vmURL(digestName))
let preserveExplicitlyPulledMark = digestVMDir.isExplicitlyPulled()
// Open an existing VM directory corresponding to this name, if any,
// marking it as outdated to speed up the garbage collection process
@@ -195,22 +345,41 @@ class VMStorageOCI: PrunableStorage {
let tmpVMDirLock = try FileLock(lockURL: tmpVMDir.baseURL)
try tmpVMDirLock.lock()
// A previously pulled standalone image already has the complete base
// disk locally as disk.img. Promote that file into the content store
// before sizing or pulling so a stacked child only fetches overlays.
try reuseStandaloneDiskForStackedBaseIfPossible(manifest)
// Try to reclaim some cache space if we know the VM size in advance
if let uncompressedDiskSize = manifest.uncompressedDiskSize() {
OpenTelemetry.instance.contextProvider.activeSpan?.setAttribute(
key: "oci.image-uncompressed-disk-size-bytes",
value: .int(Int(uncompressedDiskSize))
)
if let requiredDiskStorageBytes = try requiredDiskStorageBytes(for: manifest) {
if let telemetryValue = Int(exactly: requiredDiskStorageBytes) {
OpenTelemetry.instance.contextProvider.activeSpan?.setAttribute(
key: "oci.image-required-disk-storage-bytes",
value: .int(telemetryValue)
)
}
let otherVMFilesSize: UInt64 = 128 * 1024 * 1024
let requiredStorage = requiredDiskStorageBytes.addingReportingOverflow(otherVMFilesSize)
guard !requiredStorage.overflow else {
throw RuntimeError.PullFailed("required pull storage size overflows UInt64")
}
try Prune.reclaimIfNeeded(uncompressedDiskSize + otherVMFilesSize)
try Prune.reclaimIfNeeded(requiredStorage.partialValue)
}
try await withTaskCancellationHandler(operation: {
try await retry(maxAttempts: 5) {
// Choose the best base image which has the most deduplication ratio
let localLayerCache = try await chooseLocalLayerCache(name, manifest, registry)
// Existing standalone images can still reuse another complete local disk.
// Stacked images reconstruct their immutable files through the
// shared content store instead of materializing disk.img.
let localLayerCache: LocalLayerCache?
switch try manifest.tartDiskRepresentation() {
case .flat:
localLayerCache = try await chooseLocalLayerCache(name, manifest, registry)
case .stacked:
localLayerCache = nil
}
if let llc = localLayerCache {
let deduplicatedHuman = ByteCountFormatter.string(fromByteCount: Int64(llc.deduplicatedBytes), countStyle: .file)
@@ -232,6 +401,14 @@ class VMStorageOCI: PrunableStorage {
return .throw
}
// Preserve the exact manifest bytes received from the registry. Its
// digest identifies this cached image and stacked VMs pin it.
try manifestData.write(to: tmpVMDir.manifestURL)
if preserveExplicitlyPulledMark {
tmpVMDir.markExplicitlyPulled()
}
try move(digestName, from: tmpVMDir)
}, onCancel: {
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
@@ -253,6 +430,82 @@ class VMStorageOCI: PrunableStorage {
_ = try VMStorageOCI().open(name)
}
/// Returns `nil` for standalone images and the missing immutable disk-file groups
/// for stacked images. `ContentStore.existingContentURL()` intentionally
/// validates the digest so corrupt entries are repaired by a normal pull.
private func missingStackedDiskFileGroups(for manifest: OCIManifest) throws -> [TartDiskFileGroup]? {
guard case .stacked(let base, let overlays) = try manifest.tartDiskRepresentation() else {
return nil
}
let contentStore = try ContentStore()
var missingGroups: [TartDiskFileGroup] = []
for group in [base] + overlays {
guard let contentDigest = group.contentDigest else {
throw OCIManifestValidationError.invalidDiskMetadata("stacked disk files need a whole-file content digest")
}
if try contentStore.existingContentURL(for: contentDigest) == nil {
missingGroups.append(group)
}
}
return missingGroups
}
/// Seed a stacked image's immutable base from an already pulled standalone
/// OCI record when both manifests describe the same transport chunks. The
/// content store still verifies the whole-file digest before publishing it.
func reuseStandaloneDiskForStackedBaseIfPossible(_ manifest: OCIManifest) throws {
guard case .stacked(let base, _) = try manifest.tartDiskRepresentation(),
let contentDigest = base.contentDigest else {
return
}
let contentStore = try ContentStore()
// Content-store entries are verified when installed. Avoid hashing a
// potentially large prewarmed base again on every stacked pull.
guard try contentStore.contentURLIfPresent(for: contentDigest) == nil else {
return
}
for (_, vmDir, isSymlink) in try list() where !isSymlink && vmDir.isStandalone {
guard let manifestData = try? Data(contentsOf: vmDir.manifestURL),
let candidateManifest = try? OCIManifest(fromJSON: manifestData),
case .flat(let candidateBase) = try? candidateManifest.tartDiskRepresentation(),
diskChunksMatch(candidateBase.chunks, base.chunks) else {
continue
}
let temporaryURL = try contentStore.temporaryContentURL(for: contentDigest)
do {
try FileManager.default.copyItem(at: vmDir.diskURL, to: temporaryURL)
_ = try contentStore.install(temporaryURL, contentDigest: contentDigest)
return
} catch ContentStoreError.contentDigestMismatch {
try? FileManager.default.removeItem(at: temporaryURL)
} catch {
try? FileManager.default.removeItem(at: temporaryURL)
throw error
}
}
}
/// Compare the OCI transport identity while ignoring stacked-only
/// whole-file annotations added to the first base chunk.
private func diskChunksMatch(_ left: [OCIManifestLayer], _ right: [OCIManifestLayer]) -> Bool {
guard left.count == right.count else {
return false
}
return zip(left, right).allSatisfy { left, right in
left.mediaType == right.mediaType &&
left.size == right.size &&
left.digest == right.digest &&
left.uncompressedSize() == right.uncompressedSize() &&
left.uncompressedContentDigest() == right.uncompressedContentDigest()
}
}
func linked(from: RemoteName, to: RemoteName) -> Bool {
do {
let resolvedFrom = try FileManager.default.destinationOfSymbolicLink(atPath: vmURL(from).path)
@@ -280,10 +533,16 @@ class VMStorageOCI: PrunableStorage {
}
// Load OCI VM images and their manifests (if present)
var candidates: [(name: String, vmDir: VMDirectory, manifest: OCIManifest, deduplicatedBytes: UInt64)] = []
var candidates: [(
name: String,
vmDir: VMDirectory,
manifest: OCIManifest,
manifestDigest: String,
deduplicatedBytes: UInt64
)] = []
for (name, vmDir, isSymlink) in try list() {
if isSymlink {
if isSymlink || !vmDir.isStandalone {
continue
}
@@ -295,7 +554,13 @@ class VMStorageOCI: PrunableStorage {
continue
}
candidates.append((name, vmDir, manifest, calculateDeduplicatedBytes(manifest)))
candidates.append((
name,
vmDir,
manifest,
Digest.hash(manifestJSON),
calculateDeduplicatedBytes(manifest)
))
}
// Previously we haven't stored the OCI VM image manifests, but still fetched the VM image manifest if
@@ -305,10 +570,17 @@ class VMStorageOCI: PrunableStorage {
// with the registry if we haven't already retrieved the manifest for that OCI VM image.
if name.reference.type == .Tag,
let vmDir = try? open(name),
vmDir.isStandalone,
let digest = try? digest(name),
try !candidates.contains(where: {try $0.manifest.digest() == digest}),
let (manifest, _) = try? await registry.pullManifest(reference: digest) {
candidates.append((name.description, vmDir, manifest, calculateDeduplicatedBytes(manifest)))
!candidates.contains(where: { $0.manifestDigest == digest }),
let (manifest, manifestData) = try? await registry.pullManifest(reference: digest) {
candidates.append((
name.description,
vmDir,
manifest,
Digest.hash(manifestData),
calculateDeduplicatedBytes(manifest)
))
}
// Now, find the best match based on how many bytes we'll deduplicate